diff options
Diffstat (limited to 'src/wallet/wallet2.cpp')
-rw-r--r-- | src/wallet/wallet2.cpp | 245 |
1 files changed, 245 insertions, 0 deletions
diff --git a/src/wallet/wallet2.cpp b/src/wallet/wallet2.cpp index 2af41f588..963783002 100644 --- a/src/wallet/wallet2.cpp +++ b/src/wallet/wallet2.cpp @@ -7947,6 +7947,251 @@ bool wallet2::check_tx_proof(const crypto::hash &txid, const cryptonote::account return false; } +std::string wallet2::get_reserve_proof(const boost::optional<std::pair<uint32_t, uint64_t>> &account_minreserve, const std::string &message) +{ + THROW_WALLET_EXCEPTION_IF(m_watch_only || m_multisig, error::wallet_internal_error, "Reserve proof can only be generated by a full wallet"); + THROW_WALLET_EXCEPTION_IF(balance_all() == 0, error::wallet_internal_error, "Zero balance"); + THROW_WALLET_EXCEPTION_IF(account_minreserve && balance(account_minreserve->first) < account_minreserve->second, error::wallet_internal_error, + "Not enough balance in this account for the requested minimum reserve amount"); + + // determine which outputs to include in the proof + std::vector<size_t> selected_transfers; + for (size_t i = 0; i < m_transfers.size(); ++i) + { + const transfer_details &td = m_transfers[i]; + if (!td.m_spent && (!account_minreserve || account_minreserve->first == td.m_subaddr_index.major)) + selected_transfers.push_back(i); + } + + if (account_minreserve) + { + // minimize the number of outputs included in the proof, by only picking the N largest outputs that can cover the requested min reserve amount + std::sort(selected_transfers.begin(), selected_transfers.end(), [&](const size_t a, const size_t b) + { return m_transfers[a].amount() > m_transfers[b].amount(); }); + while (selected_transfers.size() >= 2 && m_transfers[selected_transfers[1]].amount() >= account_minreserve->second) + selected_transfers.erase(selected_transfers.begin()); + size_t sz = 0; + uint64_t total = 0; + while (total < account_minreserve->second) + { + total += m_transfers[selected_transfers[sz]].amount(); + ++sz; + } + selected_transfers.resize(sz); + } + + // compute signature prefix hash + std::string prefix_data = message; + prefix_data.append((const char*)&m_account.get_keys().m_account_address, sizeof(cryptonote::account_public_address)); + for (size_t i = 0; i < selected_transfers.size(); ++i) + { + prefix_data.append((const char*)&m_transfers[selected_transfers[i]].m_key_image, sizeof(crypto::key_image)); + } + crypto::hash prefix_hash; + crypto::cn_fast_hash(prefix_data.data(), prefix_data.size(), prefix_hash); + + // generate proof entries + std::vector<reserve_proof_entry> proofs(selected_transfers.size()); + std::unordered_set<cryptonote::subaddress_index> subaddr_indices = { {0,0} }; + for (size_t i = 0; i < selected_transfers.size(); ++i) + { + const transfer_details &td = m_transfers[selected_transfers[i]]; + reserve_proof_entry& proof = proofs[i]; + proof.txid = td.m_txid; + proof.index_in_tx = td.m_internal_output_index; + proof.key_image = td.m_key_image; + subaddr_indices.insert(td.m_subaddr_index); + + // get tx pub key + const crypto::public_key tx_pub_key = get_tx_pub_key_from_extra(td.m_tx, td.m_pk_index); + THROW_WALLET_EXCEPTION_IF(tx_pub_key == crypto::null_pkey, error::wallet_internal_error, "The tx public key isn't found"); + const std::vector<crypto::public_key> additional_tx_pub_keys = get_additional_tx_pub_keys_from_extra(td.m_tx); + + // determine which tx pub key was used for deriving the output key + const crypto::public_key *tx_pub_key_used = &tx_pub_key; + for (int i = 0; i < 2; ++i) + { + proof.shared_secret = rct::rct2pk(rct::scalarmultKey(rct::pk2rct(*tx_pub_key_used), rct::sk2rct(m_account.get_keys().m_view_secret_key))); + crypto::key_derivation derivation; + THROW_WALLET_EXCEPTION_IF(!crypto::generate_key_derivation(proof.shared_secret, rct::rct2sk(rct::I), derivation), + error::wallet_internal_error, "Failed to generate key derivation"); + crypto::public_key subaddress_spendkey; + THROW_WALLET_EXCEPTION_IF(!derive_subaddress_public_key(td.get_public_key(), derivation, proof.index_in_tx, subaddress_spendkey), + error::wallet_internal_error, "Failed to derive subaddress public key"); + if (m_subaddresses.count(subaddress_spendkey) == 1) + break; + THROW_WALLET_EXCEPTION_IF(additional_tx_pub_keys.empty(), error::wallet_internal_error, + "Normal tx pub key doesn't derive the expected output, while the additional tx pub keys are empty"); + THROW_WALLET_EXCEPTION_IF(i == 1, error::wallet_internal_error, + "Neither normal tx pub key nor additional tx pub key derive the expected output key"); + tx_pub_key_used = &additional_tx_pub_keys[proof.index_in_tx]; + } + + // generate signature for shared secret + crypto::generate_tx_proof(prefix_hash, m_account.get_keys().m_account_address.m_view_public_key, *tx_pub_key_used, boost::none, proof.shared_secret, m_account.get_keys().m_view_secret_key, proof.shared_secret_sig); + + // derive ephemeral secret key + crypto::key_image ki; + cryptonote::keypair ephemeral; + const bool r = cryptonote::generate_key_image_helper(m_account.get_keys(), m_subaddresses, td.get_public_key(), tx_pub_key, additional_tx_pub_keys, td.m_internal_output_index, ephemeral, ki); + THROW_WALLET_EXCEPTION_IF(!r, error::wallet_internal_error, "Failed to generate key image"); + THROW_WALLET_EXCEPTION_IF(ephemeral.pub != td.get_public_key(), error::wallet_internal_error, "Derived public key doesn't agree with the stored one"); + + // generate signature for key image + const std::vector<const crypto::public_key*> pubs = { &ephemeral.pub }; + crypto::generate_ring_signature(prefix_hash, td.m_key_image, &pubs[0], 1, ephemeral.sec, 0, &proof.key_image_sig); + } + + // collect all subaddress spend keys that received those outputs and generate their signatures + std::unordered_map<crypto::public_key, crypto::signature> subaddr_spendkeys; + for (const cryptonote::subaddress_index &index : subaddr_indices) + { + crypto::secret_key subaddr_spend_skey = m_account.get_keys().m_spend_secret_key; + if (!index.is_zero()) + { + crypto::secret_key m = cryptonote::get_subaddress_secret_key(m_account.get_keys().m_view_secret_key, index); + crypto::secret_key tmp = subaddr_spend_skey; + sc_add((unsigned char*)&subaddr_spend_skey, (unsigned char*)&m, (unsigned char*)&tmp); + } + crypto::public_key subaddr_spend_pkey; + secret_key_to_public_key(subaddr_spend_skey, subaddr_spend_pkey); + crypto::generate_signature(prefix_hash, subaddr_spend_pkey, subaddr_spend_skey, subaddr_spendkeys[subaddr_spend_pkey]); + } + + // serialize & encode + std::ostringstream oss; + boost::archive::portable_binary_oarchive ar(oss); + ar << proofs << subaddr_spendkeys; + return "ReserveProofV1" + tools::base58::encode(oss.str()); +} + +bool wallet2::check_reserve_proof(const cryptonote::account_public_address &address, const std::string &message, const std::string &sig_str, uint64_t &total, uint64_t &spent) +{ + uint32_t rpc_version; + THROW_WALLET_EXCEPTION_IF(!check_connection(&rpc_version), error::wallet_internal_error, "Failed to connect to daemon: " + get_daemon_address()); + THROW_WALLET_EXCEPTION_IF(rpc_version < MAKE_CORE_RPC_VERSION(1, 0), error::wallet_internal_error, "Daemon RPC version is too old"); + + static constexpr char header[] = "ReserveProofV1"; + THROW_WALLET_EXCEPTION_IF(!boost::string_ref{sig_str}.starts_with(header), error::wallet_internal_error, + "Signature header check error"); + + std::string sig_decoded; + THROW_WALLET_EXCEPTION_IF(!tools::base58::decode(sig_str.substr(std::strlen(header)), sig_decoded), error::wallet_internal_error, + "Signature decoding error"); + + std::istringstream iss(sig_decoded); + boost::archive::portable_binary_iarchive ar(iss); + std::vector<reserve_proof_entry> proofs; + std::unordered_map<crypto::public_key, crypto::signature> subaddr_spendkeys; + ar >> proofs >> subaddr_spendkeys; + + THROW_WALLET_EXCEPTION_IF(subaddr_spendkeys.count(address.m_spend_public_key) == 0, error::wallet_internal_error, + "The given address isn't found in the proof"); + + // compute signature prefix hash + std::string prefix_data = message; + prefix_data.append((const char*)&address, sizeof(cryptonote::account_public_address)); + for (size_t i = 0; i < proofs.size(); ++i) + { + prefix_data.append((const char*)&proofs[i].key_image, sizeof(crypto::key_image)); + } + crypto::hash prefix_hash; + crypto::cn_fast_hash(prefix_data.data(), prefix_data.size(), prefix_hash); + + // fetch txes from daemon + COMMAND_RPC_GET_TRANSACTIONS::request gettx_req; + COMMAND_RPC_GET_TRANSACTIONS::response gettx_res; + for (size_t i = 0; i < proofs.size(); ++i) + gettx_req.txs_hashes.push_back(epee::string_tools::pod_to_hex(proofs[i].txid)); + m_daemon_rpc_mutex.lock(); + bool ok = net_utils::invoke_http_json("/gettransactions", gettx_req, gettx_res, m_http_client); + m_daemon_rpc_mutex.unlock(); + THROW_WALLET_EXCEPTION_IF(!ok || gettx_res.txs.size() != proofs.size(), + error::wallet_internal_error, "Failed to get transaction from daemon"); + + // check spent status + COMMAND_RPC_IS_KEY_IMAGE_SPENT::request kispent_req; + COMMAND_RPC_IS_KEY_IMAGE_SPENT::response kispent_res; + for (size_t i = 0; i < proofs.size(); ++i) + kispent_req.key_images.push_back(epee::string_tools::pod_to_hex(proofs[i].key_image)); + m_daemon_rpc_mutex.lock(); + ok = epee::net_utils::invoke_http_json("/is_key_image_spent", kispent_req, kispent_res, m_http_client, rpc_timeout); + m_daemon_rpc_mutex.unlock(); + THROW_WALLET_EXCEPTION_IF(!ok || kispent_res.spent_status.size() != proofs.size(), + error::wallet_internal_error, "Failed to get key image spent status from daemon"); + + total = spent = 0; + for (size_t i = 0; i < proofs.size(); ++i) + { + const reserve_proof_entry& proof = proofs[i]; + THROW_WALLET_EXCEPTION_IF(gettx_res.txs[i].in_pool, error::wallet_internal_error, "Tx is unconfirmed"); + + cryptonote::blobdata tx_data; + ok = string_tools::parse_hexstr_to_binbuff(gettx_res.txs[i].as_hex, tx_data); + THROW_WALLET_EXCEPTION_IF(!ok, error::wallet_internal_error, "Failed to parse transaction from daemon"); + + crypto::hash tx_hash, tx_prefix_hash; + cryptonote::transaction tx; + THROW_WALLET_EXCEPTION_IF(!cryptonote::parse_and_validate_tx_from_blob(tx_data, tx, tx_hash, tx_prefix_hash), error::wallet_internal_error, + "Failed to validate transaction from daemon"); + THROW_WALLET_EXCEPTION_IF(tx_hash != proof.txid, error::wallet_internal_error, "Failed to get the right transaction from daemon"); + + THROW_WALLET_EXCEPTION_IF(proof.index_in_tx >= tx.vout.size(), error::wallet_internal_error, "index_in_tx is out of bound"); + + const cryptonote::txout_to_key* const out_key = boost::get<cryptonote::txout_to_key>(std::addressof(tx.vout[proof.index_in_tx].target)); + THROW_WALLET_EXCEPTION_IF(!out_key, error::wallet_internal_error, "Output key wasn't found") + + // get tx pub key + const crypto::public_key tx_pub_key = get_tx_pub_key_from_extra(tx); + THROW_WALLET_EXCEPTION_IF(tx_pub_key == crypto::null_pkey, error::wallet_internal_error, "The tx public key isn't found"); + const std::vector<crypto::public_key> additional_tx_pub_keys = get_additional_tx_pub_keys_from_extra(tx); + + // check singature for shared secret + ok = crypto::check_tx_proof(prefix_hash, address.m_view_public_key, tx_pub_key, boost::none, proof.shared_secret, proof.shared_secret_sig); + if (!ok && additional_tx_pub_keys.size() == tx.vout.size()) + ok = crypto::check_tx_proof(prefix_hash, address.m_view_public_key, additional_tx_pub_keys[proof.index_in_tx], boost::none, proof.shared_secret, proof.shared_secret_sig); + if (!ok) + return false; + + // check signature for key image + const std::vector<const crypto::public_key*> pubs = { &out_key->key }; + ok = crypto::check_ring_signature(prefix_hash, proof.key_image, &pubs[0], 1, &proof.key_image_sig); + if (!ok) + return false; + + // check if the address really received the fund + crypto::key_derivation derivation; + THROW_WALLET_EXCEPTION_IF(!crypto::generate_key_derivation(proof.shared_secret, rct::rct2sk(rct::I), derivation), error::wallet_internal_error, "Failed to generate key derivation"); + crypto::public_key subaddr_spendkey; + crypto::derive_subaddress_public_key(out_key->key, derivation, proof.index_in_tx, subaddr_spendkey); + THROW_WALLET_EXCEPTION_IF(subaddr_spendkeys.count(subaddr_spendkey) == 0, error::wallet_internal_error, + "The address doesn't seem to have received the fund"); + + // check amount + uint64_t amount = tx.vout[proof.index_in_tx].amount; + if (amount == 0) + { + // decode rct + crypto::secret_key shared_secret; + crypto::derivation_to_scalar(derivation, proof.index_in_tx, shared_secret); + rct::ecdhTuple ecdh_info = tx.rct_signatures.ecdhInfo[proof.index_in_tx]; + rct::ecdhDecode(ecdh_info, rct::sk2rct(shared_secret)); + amount = rct::h2d(ecdh_info.amount); + } + total += amount; + if (kispent_res.spent_status[i]) + spent += amount; + } + + // check signatures for all subaddress spend keys + for (const auto &i : subaddr_spendkeys) + { + if (!crypto::check_signature(prefix_hash, i.first, i.second)) + return false; + } + return true; +} + std::string wallet2::get_wallet_file() const { return m_wallet_file; |