diff options
Diffstat (limited to 'src/wallet')
-rw-r--r-- | src/wallet/api/wallet.cpp | 69 | ||||
-rw-r--r-- | src/wallet/api/wallet.h | 4 | ||||
-rw-r--r-- | src/wallet/api/wallet2_api.h | 17 | ||||
-rw-r--r-- | src/wallet/ringdb.cpp | 4 | ||||
-rw-r--r-- | src/wallet/wallet2.cpp | 476 | ||||
-rw-r--r-- | src/wallet/wallet2.h | 17 | ||||
-rw-r--r-- | src/wallet/wallet_errors.h | 9 | ||||
-rw-r--r-- | src/wallet/wallet_rpc_server.cpp | 227 | ||||
-rw-r--r-- | src/wallet/wallet_rpc_server.h | 5 | ||||
-rw-r--r-- | src/wallet/wallet_rpc_server_commands_defs.h | 92 | ||||
-rw-r--r-- | src/wallet/wallet_rpc_server_error_codes.h | 1 |
11 files changed, 788 insertions, 133 deletions
diff --git a/src/wallet/api/wallet.cpp b/src/wallet/api/wallet.cpp index 236928348..ddf2d74ff 100644 --- a/src/wallet/api/wallet.cpp +++ b/src/wallet/api/wallet.cpp @@ -96,6 +96,9 @@ namespace { throw runtime_error("Multisig wallet is not finalized yet"); } } + void checkMultisigWalletReady(const std::unique_ptr<tools::wallet2> &wallet) { + return checkMultisigWalletReady(wallet.get()); + } void checkMultisigWalletNotReady(const tools::wallet2* wallet) { if (!wallet) { @@ -111,6 +114,9 @@ namespace { throw runtime_error("Multisig wallet is already finalized"); } } + void checkMultisigWalletNotReady(const std::unique_ptr<tools::wallet2> &wallet) { + return checkMultisigWalletNotReady(wallet.get()); + } } struct Wallet2CallbackImpl : public tools::i_wallet2_callback @@ -375,16 +381,17 @@ WalletImpl::WalletImpl(NetworkType nettype, uint64_t kdf_rounds) , m_synchronized(false) , m_rebuildWalletCache(false) , m_is_connected(false) + , m_refreshShouldRescan(false) { - m_wallet = std::make_unique<tools::wallet2>(static_cast<cryptonote::network_type>(nettype), kdf_rounds, true); - m_history = std::make_unique<TransactionHistoryImpl>(this); - m_wallet2Callback = std::make_unique<Wallet2CallbackImpl>(this); - m_wallet->callback(m_wallet2Callback); + m_wallet.reset(new tools::wallet2(static_cast<cryptonote::network_type>(nettype), kdf_rounds, true)); + m_history.reset(new TransactionHistoryImpl(this)); + m_wallet2Callback.reset(new Wallet2CallbackImpl(this)); + m_wallet->callback(m_wallet2Callback.get()); m_refreshThreadDone = false; m_refreshEnabled = false; - m_addressBook = std::make_unique<AddressBookImpl>(this); - m_subaddress = std::make_unique<SubaddressImpl>(this); - m_subaddressAccount = std::make_unique<SubaddressAccountImpl>(this); + m_addressBook.reset(new AddressBookImpl(this)); + m_subaddress.reset(new SubaddressImpl(this)); + m_subaddressAccount.reset(new SubaddressAccountImpl(this)); m_refreshIntervalMillis = DEFAULT_REFRESH_INTERVAL_MILLIS; @@ -399,6 +406,7 @@ WalletImpl::~WalletImpl() { LOG_PRINT_L1(__FUNCTION__); + m_wallet->callback(NULL); // Pause refresh thread - prevents refresh from starting again pauseRefresh(); // Close wallet - stores cache and stops ongoing refresh operation @@ -769,7 +777,7 @@ bool WalletImpl::setPassword(const std::string &password) { clearStatus(); try { - m_wallet->rewrite(m_wallet->get_wallet_file(), password); + m_wallet->change_password(m_wallet->get_wallet_file(), m_password, password); m_password = password; } catch (const std::exception &e) { setStatusError(e.what()); @@ -1004,6 +1012,20 @@ void WalletImpl::refreshAsync() m_refreshCV.notify_one(); } +bool WalletImpl::rescanBlockchain() +{ + clearStatus(); + m_refreshShouldRescan = true; + doRefresh(); + return status() == Status_Ok; +} + +void WalletImpl::rescanBlockchainAsync() +{ + m_refreshShouldRescan = true; + refreshAsync(); +} + void WalletImpl::setAutoRefreshInterval(int millis) { if (millis > MAX_REFRESH_INTERVAL_MILLIS) { @@ -1181,6 +1203,20 @@ string WalletImpl::makeMultisig(const vector<string>& info, uint32_t threshold) return string(); } +std::string WalletImpl::exchangeMultisigKeys(const std::vector<std::string> &info) { + try { + clearStatus(); + checkMultisigWalletNotReady(m_wallet); + + return m_wallet->exchange_multisig_keys(epee::wipeable_string(m_password), info); + } catch (const exception& e) { + LOG_ERROR("Error on exchanging multisig keys: ") << e.what(); + setStatusError(string(tr("Failed to make multisig: ")) + e.what()); + } + + return string(); +} + bool WalletImpl::finalizeMultisig(const vector<string>& extraMultisigInfo) { try { clearStatus(); @@ -1963,6 +1999,7 @@ void WalletImpl::refreshThreadFunc() LOG_PRINT_L3(__FUNCTION__ << ": refresh lock acquired..."); LOG_PRINT_L3(__FUNCTION__ << ": m_refreshEnabled: " << m_refreshEnabled); LOG_PRINT_L3(__FUNCTION__ << ": m_status: " << status()); + LOG_PRINT_L3(__FUNCTION__ << ": m_refreshShouldRescan: " << m_refreshShouldRescan); if (m_refreshEnabled) { LOG_PRINT_L3(__FUNCTION__ << ": refreshing..."); doRefresh(); @@ -1973,12 +2010,16 @@ void WalletImpl::refreshThreadFunc() void WalletImpl::doRefresh() { + bool rescan = m_refreshShouldRescan.exchange(false); // synchronizing async and sync refresh calls boost::lock_guard<boost::mutex> guarg(m_refreshMutex2); - try { + do try { + LOG_PRINT_L3(__FUNCTION__ << ": doRefresh, rescan = "<<rescan); // Syncing daemon and refreshing wallet simultaneously is very resource intensive. // Disable refresh if wallet is disconnected or daemon isn't synced. if (m_wallet->light_wallet() || daemonSynced()) { + if(rescan) + m_wallet->rescan_blockchain(false); m_wallet->refresh(trustedDaemon()); if (!m_synchronized) { m_synchronized = true; @@ -1995,7 +2036,9 @@ void WalletImpl::doRefresh() } } catch (const std::exception &e) { setStatusError(e.what()); - } + break; + }while(!rescan && (rescan=m_refreshShouldRescan.exchange(false))); // repeat if not rescanned and rescan was requested + if (m_wallet2Callback->getListener()) { m_wallet2Callback->getListener()->refreshed(); } @@ -2140,7 +2183,7 @@ bool WalletImpl::blackballOutputs(const std::vector<std::string> &outputs, bool bool ret = m_wallet->set_blackballed_outputs(raw_outputs, add); if (!ret) { - setStatusError(tr("Failed to set blackballed outputs")); + setStatusError(tr("Failed to mark outputs as spent")); return false; } return true; @@ -2162,7 +2205,7 @@ bool WalletImpl::blackballOutput(const std::string &amount, const std::string &o bool ret = m_wallet->blackball_output(std::make_pair(raw_amount, raw_offset)); if (!ret) { - setStatusError(tr("Failed to blackball output")); + setStatusError(tr("Failed to mark output as spent")); return false; } return true; @@ -2184,7 +2227,7 @@ bool WalletImpl::unblackballOutput(const std::string &amount, const std::string bool ret = m_wallet->unblackball_output(std::make_pair(raw_amount, raw_offset)); if (!ret) { - setStatusError(tr("Failed to unblackball output")); + setStatusError(tr("Failed to mark output as unspent")); return false; } return true; diff --git a/src/wallet/api/wallet.h b/src/wallet/api/wallet.h index 8e2af347d..b4637b8e6 100644 --- a/src/wallet/api/wallet.h +++ b/src/wallet/api/wallet.h @@ -114,6 +114,8 @@ public: bool synchronized() const override; bool refresh() override; void refreshAsync() override; + bool rescanBlockchain() override; + void rescanBlockchainAsync() override; void setAutoRefreshInterval(int millis) override; int autoRefreshInterval() const override; void setRefreshFromBlockHeight(uint64_t refresh_from_block_height) override; @@ -137,6 +139,7 @@ public: MultisigState multisig() const override; std::string getMultisigInfo() const override; std::string makeMultisig(const std::vector<std::string>& info, uint32_t threshold) override; + std::string exchangeMultisigKeys(const std::vector<std::string> &info) override; bool finalizeMultisig(const std::vector<std::string>& extraMultisigInfo) override; bool exportMultisigImages(std::string& images) override; size_t importMultisigImages(const std::vector<std::string>& images) override; @@ -231,6 +234,7 @@ private: std::atomic<bool> m_refreshEnabled; std::atomic<bool> m_refreshThreadDone; std::atomic<int> m_refreshIntervalMillis; + std::atomic<bool> m_refreshShouldRescan; // synchronizing refresh loop; boost::mutex m_refreshMutex; diff --git a/src/wallet/api/wallet2_api.h b/src/wallet/api/wallet2_api.h index 68ea26262..82627de29 100644 --- a/src/wallet/api/wallet2_api.h +++ b/src/wallet/api/wallet2_api.h @@ -644,6 +644,17 @@ struct Wallet virtual void refreshAsync() = 0; /** + * @brief rescanBlockchain - rescans the wallet, updating transactions from daemon + * @return - true if refreshed successfully; + */ + virtual bool rescanBlockchain() = 0; + + /** + * @brief rescanBlockchainAsync - rescans wallet asynchronously, starting from genesys + */ + virtual void rescanBlockchainAsync() = 0; + + /** * @brief setAutoRefreshInterval - setup interval for automatic refresh. * @param seconds - interval in millis. if zero or less than zero - automatic refresh disabled; */ @@ -707,6 +718,12 @@ struct Wallet */ virtual std::string makeMultisig(const std::vector<std::string>& info, uint32_t threshold) = 0; /** + * @brief exchange_multisig_keys - provides additional key exchange round for arbitrary multisig schemes (like N-1/N, M/N) + * @param info - base58 encoded key derivations returned by makeMultisig or exchangeMultisigKeys function call + * @return new info string if more rounds required or an empty string if wallet creation is done + */ + virtual std::string exchangeMultisigKeys(const std::vector<std::string> &info) = 0; + /** * @brief finalizeMultisig - finalizes N - 1 / N multisig wallets creation * @param extraMultisigInfo - wallet participants' extra multisig info obtained with makeMultisig call * @return true if success diff --git a/src/wallet/ringdb.cpp b/src/wallet/ringdb.cpp index e5995e7fb..f562d6c06 100644 --- a/src/wallet/ringdb.cpp +++ b/src/wallet/ringdb.cpp @@ -412,13 +412,13 @@ bool ringdb::blackball_worker(const std::vector<std::pair<uint64_t, uint64_t>> & switch (op) { case BLACKBALL_BLACKBALL: - MDEBUG("Blackballing output " << output.first << "/" << output.second); + MDEBUG("Marking output " << output.first << "/" << output.second << " as spent"); dbr = mdb_cursor_put(cursor, &key, &data, MDB_APPENDDUP); if (dbr == MDB_KEYEXIST) dbr = 0; break; case BLACKBALL_UNBLACKBALL: - MDEBUG("Unblackballing output " << output.first << "/" << output.second); + MDEBUG("Marking output " << output.first << "/" << output.second << " as unspent"); dbr = mdb_cursor_get(cursor, &key, &data, MDB_GET_BOTH); if (dbr == 0) dbr = mdb_cursor_del(cursor, 0); diff --git a/src/wallet/wallet2.cpp b/src/wallet/wallet2.cpp index e6ab10756..37b60c5d7 100644 --- a/src/wallet/wallet2.cpp +++ b/src/wallet/wallet2.cpp @@ -37,6 +37,8 @@ #include <boost/algorithm/string/classification.hpp> #include <boost/algorithm/string/trim.hpp> #include <boost/algorithm/string/split.hpp> +#include <boost/algorithm/string/join.hpp> +#include <boost/range/adaptor/transformed.hpp> #include "include_base_utils.h" using namespace epee; @@ -114,14 +116,17 @@ using namespace cryptonote; #define OUTPUT_EXPORT_FILE_MAGIC "Monero output export\003" -#define SEGREGATION_FORK_HEIGHT 1546000 -#define TESTNET_SEGREGATION_FORK_HEIGHT 1000000 -#define STAGENET_SEGREGATION_FORK_HEIGHT 1000000 +#define SEGREGATION_FORK_HEIGHT 99999999 +#define TESTNET_SEGREGATION_FORK_HEIGHT 99999999 +#define STAGENET_SEGREGATION_FORK_HEIGHT 99999999 #define SEGREGATION_FORK_VICINITY 1500 /* blocks */ #define FIRST_REFRESH_GRANULARITY 1024 +#define GAMMA_PICK_HALF_WINDOW 5 + static const std::string MULTISIG_SIGNATURE_MAGIC = "SigMultisigPkV1"; +static const std::string MULTISIG_EXTRA_INFO_MAGIC = "MultisigxV1"; namespace { @@ -133,6 +138,42 @@ namespace dir /= ".shared-ringdb"; return dir.string(); } + + std::string pack_multisignature_keys(const std::string& prefix, const std::vector<crypto::public_key>& keys, const crypto::secret_key& signer_secret_key) + { + std::string data; + crypto::public_key signer; + CHECK_AND_ASSERT_THROW_MES(crypto::secret_key_to_public_key(signer_secret_key, signer), "Failed to derive public spend key"); + data += std::string((const char *)&signer, sizeof(crypto::public_key)); + + for (const auto &key: keys) + { + data += std::string((const char *)&key, sizeof(crypto::public_key)); + } + + data.resize(data.size() + sizeof(crypto::signature)); + + crypto::hash hash; + crypto::cn_fast_hash(data.data(), data.size() - sizeof(crypto::signature), hash); + crypto::signature &signature = *(crypto::signature*)&data[data.size() - sizeof(crypto::signature)]; + crypto::generate_signature(hash, signer, signer_secret_key, signature); + + return MULTISIG_EXTRA_INFO_MAGIC + tools::base58::encode(data); + } + + std::vector<crypto::public_key> secret_keys_to_public_keys(const std::vector<crypto::secret_key>& keys) + { + std::vector<crypto::public_key> public_keys; + public_keys.reserve(keys.size()); + + std::transform(keys.begin(), keys.end(), std::back_inserter(public_keys), [] (const crypto::secret_key& k) -> crypto::public_key { + crypto::public_key p; + CHECK_AND_ASSERT_THROW_MES(crypto::secret_key_to_public_key(k, p), "Failed to derive public spend key"); + return p; + }); + + return public_keys; + } } namespace @@ -258,7 +299,7 @@ std::unique_ptr<tools::wallet2> make_basic(const boost::program_options::variabl trusted_daemon = false; if (tools::is_local_address(daemon_address)) { - MINFO(tr("Daemon is local, assuming trusted")); + MINFO(tools::wallet2::tr("Daemon is local, assuming trusted")); trusted_daemon = true; } } @@ -310,7 +351,7 @@ boost::optional<tools::password_container> get_password(const boost::program_opt THROW_WALLET_EXCEPTION_IF(!password_prompter, tools::error::wallet_internal_error, tools::wallet2::tr("no password specified; use --prompt-for-password to prompt for a password")); - return password_prompter(verify ? tr("Enter a new password for the wallet") : tr("Wallet password"), verify); + return password_prompter(verify ? tools::wallet2::tr("Enter a new password for the wallet") : tools::wallet2::tr("Wallet password"), verify); } std::pair<std::unique_ptr<tools::wallet2>, tools::password_container> generate_from_json(const std::string& json_file, const boost::program_options::variables_map& vm, bool unattended, const options& opts, const std::function<boost::optional<tools::password_container>(const char *, bool)> &password_prompter) @@ -772,10 +813,12 @@ wallet_keys_unlocker::~wallet_keys_unlocker() wallet2::wallet2(network_type nettype, uint64_t kdf_rounds, bool unattended): m_multisig_rescan_info(NULL), m_multisig_rescan_k(NULL), + m_upper_transaction_weight_limit(0), m_run(true), m_callback(0), m_trusted_daemon(false), m_nettype(nettype), + m_multisig_rounds_passed(0), m_always_confirm_transfers(true), m_print_ring_members(false), m_store_tx_info(true), @@ -803,6 +846,9 @@ wallet2::wallet2(network_type nettype, uint64_t kdf_rounds, bool unattended): m_is_initialized(false), m_kdf_rounds(kdf_rounds), is_old_file_format(false), + m_watch_only(false), + m_multisig(false), + m_multisig_threshold(0), m_node_rpc_proxy(m_http_client, m_daemon_rpc_mutex), m_subaddress_lookahead_major(SUBADDRESS_LOOKAHEAD_MAJOR), m_subaddress_lookahead_minor(SUBADDRESS_LOOKAHEAD_MINOR), @@ -2918,6 +2964,7 @@ bool wallet2::clear() m_address_book.clear(); m_subaddresses.clear(); m_subaddress_labels.clear(); + m_multisig_rounds_passed = 0; return true; } @@ -2932,6 +2979,7 @@ bool wallet2::store_keys(const std::string& keys_file_name, const epee::wipeable { std::string account_data; std::string multisig_signers; + std::string multisig_derivations; cryptonote::account_base account = m_account; crypto::chacha_key key; @@ -2984,6 +3032,14 @@ bool wallet2::store_keys(const std::string& keys_file_name, const epee::wipeable CHECK_AND_ASSERT_MES(r, false, "failed to serialize wallet multisig signers"); value.SetString(multisig_signers.c_str(), multisig_signers.length()); json.AddMember("multisig_signers", value, json.GetAllocator()); + + r = ::serialization::dump_binary(m_multisig_derivations, multisig_derivations); + CHECK_AND_ASSERT_MES(r, false, "failed to serialize wallet multisig derivations"); + value.SetString(multisig_derivations.c_str(), multisig_derivations.length()); + json.AddMember("multisig_derivations", value, json.GetAllocator()); + + value2.SetUint(m_multisig_rounds_passed); + json.AddMember("multisig_rounds_passed", value2, json.GetAllocator()); } value2.SetInt(m_always_confirm_transfers ? 1 :0); @@ -3156,6 +3212,8 @@ bool wallet2::load_keys(const std::string& keys_file_name, const epee::wipeable_ m_multisig = false; m_multisig_threshold = 0; m_multisig_signers.clear(); + m_multisig_rounds_passed = 0; + m_multisig_derivations.clear(); m_always_confirm_transfers = false; m_print_ring_members = false; m_default_mixin = 0; @@ -3214,6 +3272,8 @@ bool wallet2::load_keys(const std::string& keys_file_name, const epee::wipeable_ m_multisig = field_multisig; GET_FIELD_FROM_JSON_RETURN_ON_ERROR(json, multisig_threshold, unsigned int, Uint, m_multisig, 0); m_multisig_threshold = field_multisig_threshold; + GET_FIELD_FROM_JSON_RETURN_ON_ERROR(json, multisig_rounds_passed, unsigned int, Uint, false, 0); + m_multisig_rounds_passed = field_multisig_rounds_passed; if (m_multisig) { if (!json.HasMember("multisig_signers")) @@ -3234,6 +3294,24 @@ bool wallet2::load_keys(const std::string& keys_file_name, const epee::wipeable_ LOG_ERROR("Field multisig_signers found in JSON, but failed to parse"); return false; } + + //previous version of multisig does not have this field + if (json.HasMember("multisig_derivations")) + { + if (!json["multisig_derivations"].IsString()) + { + LOG_ERROR("Field multisig_derivations found in JSON, but not String"); + return false; + } + const char *field_multisig_derivations = json["multisig_derivations"].GetString(); + std::string multisig_derivations = std::string(field_multisig_derivations, field_multisig_derivations + json["multisig_derivations"].GetStringLength()); + r = ::serialization::parse_binary(multisig_derivations, m_multisig_derivations); + if (!r) + { + LOG_ERROR("Field multisig_derivations found in JSON, but failed to parse"); + return false; + } + } } GET_FIELD_FROM_JSON_RETURN_ON_ERROR(json, always_confirm_transfers, int, Int, false, true); m_always_confirm_transfers = field_always_confirm_transfers; @@ -3541,6 +3619,7 @@ bool wallet2::query_device(hw::device::device_type& device_type, const std::stri if (json.Parse(account_data.c_str()).HasParseError() || !json.IsObject()) crypto::chacha8(keys_file_data.account_data.data(), keys_file_data.account_data.size(), key, keys_file_data.iv, &account_data[0]); + device_type = hw::device::device_type::SOFTWARE; // The contents should be JSON if the wallet follows the new format. if (json.Parse(account_data.c_str()).HasParseError()) { @@ -3869,12 +3948,12 @@ std::string wallet2::make_multisig(const epee::wipeable_string &password, CHECK_AND_ASSERT_THROW_MES(!view_keys.empty(), "empty view keys"); CHECK_AND_ASSERT_THROW_MES(view_keys.size() == spend_keys.size(), "Mismatched view/spend key sizes"); CHECK_AND_ASSERT_THROW_MES(threshold > 1 && threshold <= spend_keys.size() + 1, "Invalid threshold"); - CHECK_AND_ASSERT_THROW_MES(threshold == spend_keys.size() || threshold == spend_keys.size() + 1, "Unsupported threshold case"); std::string extra_multisig_info; - crypto::hash hash; - - clear(); + std::vector<crypto::secret_key> multisig_keys; + rct::key spend_pkey = rct::identity(); + rct::key spend_skey; + std::vector<crypto::public_key> multisig_signers; // decrypt keys epee::misc_utils::auto_scope_leave_caller keys_reencryptor; @@ -3887,43 +3966,78 @@ std::string wallet2::make_multisig(const epee::wipeable_string &password, keys_reencryptor = epee::misc_utils::create_scope_leave_handler([&, this, chacha_key]() { m_account.encrypt_keys(chacha_key); m_account.decrypt_viewkey(chacha_key); }); } - MINFO("Creating spend key..."); - std::vector<crypto::secret_key> multisig_keys; - rct::key spend_pkey, spend_skey; + // In common multisig scheme there are 4 types of key exchange rounds: + // 1. First round is exchange of view secret keys and public spend keys. + // 2. Middle round is exchange of derivations: Ki = b * Mj, where b - spend secret key, + // M - public multisig key (in first round it equals to public spend key), K - new public multisig key. + // 3. Secret spend establishment round sets your secret multisig keys as follows: kl = H(Ml), where M - is *your* public multisig key, + // k - secret multisig key used to sign transactions. k and M are sets of keys, of course. + // And secret spend key as the sum of all participant's secret multisig keys + // 4. Last round establishes multisig wallet's public spend key. Participants exchange their public multisig keys + // and calculate common spend public key as sum of all unique participants' public multisig keys. + // Note that N/N scheme has only first round. N-1/N has 2 rounds: first and last. Common M/N has all 4 rounds. + + // IMPORTANT: wallet's public spend key is not equal to secret_spend_key * G! + // Wallet's public spend key is the sum of unique public multisig keys of all participants. + // secret_spend_key * G = public signer key + if (threshold == spend_keys.size() + 1) { + // In N / N case we only need to do one round and calculate secret multisig keys and new secret spend key + MINFO("Creating spend key..."); + + // Calculates all multisig keys and spend key cryptonote::generate_multisig_N_N(get_account().get_keys(), spend_keys, multisig_keys, spend_skey, spend_pkey); + + // Our signer key is b * G, where b is secret spend key. + multisig_signers = spend_keys; + multisig_signers.push_back(get_multisig_signer_public_key(get_account().get_keys().m_spend_secret_key)); } - else if (threshold == spend_keys.size()) + else { - cryptonote::generate_multisig_N1_N(get_account().get_keys(), spend_keys, multisig_keys, spend_skey, spend_pkey); + // We just got public spend keys of all participants and deriving multisig keys (set of Mi = b * Bi). + // note that derivations are public keys as DH exchange suppose it to be + auto derivations = cryptonote::generate_multisig_derivations(get_account().get_keys(), spend_keys); - // We need an extra step, so we package all the composite public keys - // we know about, and make a signed string out of them - std::string data; - crypto::public_key signer; - CHECK_AND_ASSERT_THROW_MES(crypto::secret_key_to_public_key(rct::rct2sk(spend_skey), signer), "Failed to derive public spend key"); - data += std::string((const char *)&signer, sizeof(crypto::public_key)); + spend_pkey = rct::identity(); + multisig_signers = std::vector<crypto::public_key>(spend_keys.size() + 1, crypto::null_pkey); - for (const auto &msk: multisig_keys) + if (threshold == spend_keys.size()) { - rct::key pmsk = rct::scalarmultBase(rct::sk2rct(msk)); - data += std::string((const char *)&pmsk, sizeof(crypto::public_key)); + // N - 1 / N case + + // We need an extra step, so we package all the composite public keys + // we know about, and make a signed string out of them + MINFO("Creating spend key..."); + + // Calculating set of our secret multisig keys as follows: mi = H(Mi), + // where mi - secret multisig key, Mi - others' participants public multisig key + multisig_keys = cryptonote::calculate_multisig_keys(derivations); + + // calculating current participant's spend secret key as sum of all secret multisig keys for current participant. + // IMPORTANT: participant's secret spend key is not an entire wallet's secret spend! + // Entire wallet's secret spend is sum of all unique secret multisig keys + // among all of participants and is not held by anyone! + spend_skey = rct::sk2rct(cryptonote::calculate_multisig_signer_key(multisig_keys)); + + // Preparing data for the last round to calculate common public spend key. The data contains public multisig keys. + extra_multisig_info = pack_multisignature_keys(MULTISIG_EXTRA_INFO_MAGIC, secret_keys_to_public_keys(multisig_keys), rct::rct2sk(spend_skey)); } + else + { + // M / N case + MINFO("Preparing keys for next exchange round..."); - data.resize(data.size() + sizeof(crypto::signature)); - crypto::cn_fast_hash(data.data(), data.size() - sizeof(signature), hash); - crypto::signature &signature = *(crypto::signature*)&data[data.size() - sizeof(crypto::signature)]; - crypto::generate_signature(hash, signer, rct::rct2sk(spend_skey), signature); + // Preparing data for middle round - packing new public multisig keys to exchage with others. + extra_multisig_info = pack_multisignature_keys(MULTISIG_EXTRA_INFO_MAGIC, derivations, m_account.get_keys().m_spend_secret_key); + spend_skey = rct::sk2rct(m_account.get_keys().m_spend_secret_key); - extra_multisig_info = std::string("MultisigxV1") + tools::base58::encode(data); - } - else - { - CHECK_AND_ASSERT_THROW_MES(false, "Unsupported threshold case"); + // Need to store middle keys to be able to proceed in case of wallet shutdown. + m_multisig_derivations = derivations; + } } - // the multisig view key is shared by all, make one all can derive + clear(); MINFO("Creating view key..."); crypto::secret_key view_skey = cryptonote::generate_multisig_view_secret_key(get_account().get_keys().m_view_secret_key, view_keys); @@ -3935,18 +4049,10 @@ std::string wallet2::make_multisig(const epee::wipeable_string &password, m_account_public_address = m_account.get_keys().m_account_address; m_watch_only = false; m_multisig = true; - m_multisig_threshold = threshold; m_key_device_type = hw::device::device_type::SOFTWARE; - - if (threshold == spend_keys.size() + 1) - { - m_multisig_signers = spend_keys; - m_multisig_signers.push_back(get_multisig_signer_public_key()); - } - else - { - m_multisig_signers = std::vector<crypto::public_key>(spend_keys.size() + 1, crypto::null_pkey); - } + m_multisig_threshold = threshold; + m_multisig_signers = multisig_signers; + ++m_multisig_rounds_passed; // re-encrypt keys keys_reencryptor = epee::misc_utils::auto_scope_leave_caller(); @@ -3961,13 +4067,147 @@ std::string wallet2::make_multisig(const epee::wipeable_string &password, return extra_multisig_info; } -std::string wallet2::make_multisig(const epee::wipeable_string &password, - const std::vector<std::string> &info, - uint32_t threshold) +std::string wallet2::exchange_multisig_keys(const epee::wipeable_string &password, + const std::vector<std::string> &info) +{ + THROW_WALLET_EXCEPTION_IF(info.empty(), + error::wallet_internal_error, "Empty multisig info"); + + if (info[0].substr(0, MULTISIG_EXTRA_INFO_MAGIC.size()) != MULTISIG_EXTRA_INFO_MAGIC) + { + THROW_WALLET_EXCEPTION_IF(false, + error::wallet_internal_error, "Unsupported info string"); + } + + std::vector<crypto::public_key> signers; + std::unordered_set<crypto::public_key> pkeys; + + THROW_WALLET_EXCEPTION_IF(!unpack_extra_multisig_info(info, signers, pkeys), + error::wallet_internal_error, "Bad extra multisig info"); + + return exchange_multisig_keys(password, pkeys, signers); +} + +std::string wallet2::exchange_multisig_keys(const epee::wipeable_string &password, + std::unordered_set<crypto::public_key> derivations, + std::vector<crypto::public_key> signers) +{ + CHECK_AND_ASSERT_THROW_MES(!derivations.empty(), "empty pkeys"); + CHECK_AND_ASSERT_THROW_MES(!signers.empty(), "empty signers"); + + bool ready = false; + CHECK_AND_ASSERT_THROW_MES(multisig(&ready), "The wallet is not multisig"); + CHECK_AND_ASSERT_THROW_MES(!ready, "Multisig wallet creation process has already been finished"); + + // keys are decrypted + epee::misc_utils::auto_scope_leave_caller keys_reencryptor; + if (m_ask_password == AskPasswordToDecrypt && !m_unattended && !m_watch_only) + { + crypto::chacha_key chacha_key; + crypto::generate_chacha_key(password.data(), password.size(), chacha_key, m_kdf_rounds); + m_account.encrypt_viewkey(chacha_key); + m_account.decrypt_keys(chacha_key); + keys_reencryptor = epee::misc_utils::create_scope_leave_handler([&, this, chacha_key]() { m_account.encrypt_keys(chacha_key); m_account.decrypt_viewkey(chacha_key); }); + } + + if (m_multisig_rounds_passed == multisig_rounds_required(m_multisig_signers.size(), m_multisig_threshold) - 1) + { + // the last round is passed and we have to calculate spend public key + // add ours if not included + crypto::public_key local_signer = get_multisig_signer_public_key(); + + if (std::find(signers.begin(), signers.end(), local_signer) == signers.end()) + { + signers.push_back(local_signer); + for (const auto &msk: get_account().get_multisig_keys()) + { + derivations.insert(rct::rct2pk(rct::scalarmultBase(rct::sk2rct(msk)))); + } + } + + CHECK_AND_ASSERT_THROW_MES(signers.size() == m_multisig_signers.size(), "Bad signers size"); + + // Summing all of unique public multisig keys to calculate common public spend key + crypto::public_key spend_public_key = cryptonote::generate_multisig_M_N_spend_public_key(std::vector<crypto::public_key>(derivations.begin(), derivations.end())); + m_account_public_address.m_spend_public_key = spend_public_key; + m_account.finalize_multisig(spend_public_key); + + m_multisig_signers = signers; + std::sort(m_multisig_signers.begin(), m_multisig_signers.end(), [](const crypto::public_key &e0, const crypto::public_key &e1){ return memcmp(&e0, &e1, sizeof(e0)); }); + + ++m_multisig_rounds_passed; + m_multisig_derivations.clear(); + + // keys are encrypted again + keys_reencryptor = epee::misc_utils::auto_scope_leave_caller(); + + if (!m_wallet_file.empty()) + { + bool r = store_keys(m_keys_file, password, false); + THROW_WALLET_EXCEPTION_IF(!r, error::file_save_error, m_keys_file); + + if (boost::filesystem::exists(m_wallet_file + ".address.txt")) + { + r = file_io_utils::save_string_to_file(m_wallet_file + ".address.txt", m_account.get_public_address_str(m_nettype)); + if(!r) MERROR("String with address text not saved"); + } + } + + m_subaddresses.clear(); + m_subaddress_labels.clear(); + add_subaddress_account(tr("Primary account")); + + if (!m_wallet_file.empty()) + store(); + + return {}; + } + + // Below are either middle or secret spend key establishment rounds + + for (const auto& key: m_multisig_derivations) + derivations.erase(key); + + // Deriving multisig keys (set of Mi = b * Bi) according to DH from other participants' multisig keys. + auto new_derivations = cryptonote::generate_multisig_derivations(get_account().get_keys(), std::vector<crypto::public_key>(derivations.begin(), derivations.end())); + + std::string extra_multisig_info; + if (m_multisig_rounds_passed == multisig_rounds_required(m_multisig_signers.size(), m_multisig_threshold) - 2) // next round is last + { + // Next round is last therefore we are performing secret spend establishment round as described above. + MINFO("Creating spend key..."); + + // Calculating our secret multisig keys by hashing our public multisig keys. + auto multisig_keys = cryptonote::calculate_multisig_keys(std::vector<crypto::public_key>(new_derivations.begin(), new_derivations.end())); + // And summing it to get personal secret spend key + crypto::secret_key spend_skey = cryptonote::calculate_multisig_signer_key(multisig_keys); + + m_account.make_multisig(m_account.get_keys().m_view_secret_key, spend_skey, rct::rct2pk(rct::identity()), multisig_keys); + + // Packing public multisig keys to exchange with others and calculate common public spend key in the last round + extra_multisig_info = pack_multisignature_keys(MULTISIG_EXTRA_INFO_MAGIC, secret_keys_to_public_keys(multisig_keys), spend_skey); + } + else + { + // This is just middle round + MINFO("Preparing keys for next exchange round..."); + extra_multisig_info = pack_multisignature_keys(MULTISIG_EXTRA_INFO_MAGIC, new_derivations, m_account.get_keys().m_spend_secret_key); + m_multisig_derivations = new_derivations; + } + + ++m_multisig_rounds_passed; + + create_keys_file(m_wallet_file, false, password, boost::filesystem::exists(m_wallet_file + ".address.txt")); + return extra_multisig_info; +} + +void wallet2::unpack_multisig_info(const std::vector<std::string>& info, + std::vector<crypto::public_key> &public_keys, + std::vector<crypto::secret_key> &secret_keys) const { // parse all multisig info - std::vector<crypto::secret_key> secret_keys(info.size()); - std::vector<crypto::public_key> public_keys(info.size()); + public_keys.resize(info.size()); + secret_keys.resize(info.size()); for (size_t i = 0; i < info.size(); ++i) { THROW_WALLET_EXCEPTION_IF(!verify_multisig_info(info[i], secret_keys[i], public_keys[i]), @@ -4011,75 +4251,51 @@ std::string wallet2::make_multisig(const epee::wipeable_string &password, "Found local spend public key, but not local view secret key - something very weird"); } } +} +std::string wallet2::make_multisig(const epee::wipeable_string &password, + const std::vector<std::string> &info, + uint32_t threshold) +{ + std::vector<crypto::secret_key> secret_keys(info.size()); + std::vector<crypto::public_key> public_keys(info.size()); + unpack_multisig_info(info, public_keys, secret_keys); return make_multisig(password, secret_keys, public_keys, threshold); } bool wallet2::finalize_multisig(const epee::wipeable_string &password, std::unordered_set<crypto::public_key> pkeys, std::vector<crypto::public_key> signers) { - CHECK_AND_ASSERT_THROW_MES(!pkeys.empty(), "empty pkeys"); - - // keys are decrypted - epee::misc_utils::auto_scope_leave_caller keys_reencryptor; - if (m_ask_password == AskPasswordToDecrypt && !m_unattended && !m_watch_only) - { - crypto::chacha_key chacha_key; - crypto::generate_chacha_key(password.data(), password.size(), chacha_key, m_kdf_rounds); - m_account.encrypt_viewkey(chacha_key); - m_account.decrypt_keys(chacha_key); - keys_reencryptor = epee::misc_utils::create_scope_leave_handler([&, this, chacha_key]() { m_account.encrypt_keys(chacha_key); m_account.decrypt_viewkey(chacha_key); }); - } + exchange_multisig_keys(password, pkeys, signers); + return true; +} - // add ours if not included - crypto::public_key local_signer; - CHECK_AND_ASSERT_THROW_MES(crypto::secret_key_to_public_key(get_account().get_keys().m_spend_secret_key, local_signer), - "Failed to derive public spend key"); - if (std::find(signers.begin(), signers.end(), local_signer) == signers.end()) +bool wallet2::unpack_extra_multisig_info(const std::vector<std::string>& info, + std::vector<crypto::public_key> &signers, + std::unordered_set<crypto::public_key> &pkeys) const +{ + // parse all multisig info + signers.resize(info.size(), crypto::null_pkey); + for (size_t i = 0; i < info.size(); ++i) { - signers.push_back(local_signer); - for (const auto &msk: get_account().get_multisig_keys()) - { - pkeys.insert(rct::rct2pk(rct::scalarmultBase(rct::sk2rct(msk)))); - } + if (!verify_extra_multisig_info(info[i], pkeys, signers[i])) + { + return false; + } } - CHECK_AND_ASSERT_THROW_MES(signers.size() == m_multisig_signers.size(), "Bad signers size"); - - crypto::public_key spend_public_key = cryptonote::generate_multisig_N1_N_spend_public_key(std::vector<crypto::public_key>(pkeys.begin(), pkeys.end())); - m_account_public_address.m_spend_public_key = spend_public_key; - m_account.finalize_multisig(spend_public_key); - - m_multisig_signers = signers; - std::sort(m_multisig_signers.begin(), m_multisig_signers.end(), [](const crypto::public_key &e0, const crypto::public_key &e1){ return memcmp(&e0, &e1, sizeof(e0)); }); - - // keys are encrypted again - keys_reencryptor = epee::misc_utils::auto_scope_leave_caller(); - - create_keys_file(m_wallet_file, false, password, boost::filesystem::exists(m_wallet_file + ".address.txt")); - - m_subaddresses.clear(); - m_subaddress_labels.clear(); - add_subaddress_account(tr("Primary account")); - - if (!m_wallet_file.empty()) - store(); - return true; } bool wallet2::finalize_multisig(const epee::wipeable_string &password, const std::vector<std::string> &info) { - // parse all multisig info std::unordered_set<crypto::public_key> public_keys; - std::vector<crypto::public_key> signers(info.size(), crypto::null_pkey); - for (size_t i = 0; i < info.size(); ++i) + std::vector<crypto::public_key> signers; + if (!unpack_extra_multisig_info(info, signers, public_keys)) { - if (!verify_extra_multisig_info(info[i], public_keys, signers[i])) - { - MERROR("Bad multisig info"); - return false; - } + MERROR("Bad multisig info"); + return false; } + return finalize_multisig(password, public_keys, signers); } @@ -4142,14 +4358,13 @@ bool wallet2::verify_multisig_info(const std::string &data, crypto::secret_key & bool wallet2::verify_extra_multisig_info(const std::string &data, std::unordered_set<crypto::public_key> &pkeys, crypto::public_key &signer) { - const size_t header_len = strlen("MultisigxV1"); - if (data.size() < header_len || data.substr(0, header_len) != "MultisigxV1") + if (data.size() < MULTISIG_EXTRA_INFO_MAGIC.size() || data.substr(0, MULTISIG_EXTRA_INFO_MAGIC.size()) != MULTISIG_EXTRA_INFO_MAGIC) { MERROR("Multisig info header check error"); return false; } std::string decoded; - if (!tools::base58::decode(data.substr(header_len), decoded)) + if (!tools::base58::decode(data.substr(MULTISIG_EXTRA_INFO_MAGIC.size()), decoded)) { MERROR("Multisig info decoding error"); return false; @@ -5357,10 +5572,7 @@ bool wallet2::sign_tx(unsigned_tx_set &exported_txs, std::vector<wallet2::pendin rct::RangeProofType range_proof_type = rct::RangeProofBorromean; if (sd.use_bulletproofs) { - range_proof_type = rct::RangeProofBulletproof; - for (const rct::Bulletproof &proof: ptx.tx.rct_signatures.p.bulletproofs) - if (proof.V.size() > 1) - range_proof_type = rct::RangeProofPaddedBulletproof; + range_proof_type = rct::RangeProofPaddedBulletproof; } crypto::secret_key tx_key; std::vector<crypto::secret_key> additional_tx_keys; @@ -5404,6 +5616,10 @@ bool wallet2::sign_tx(unsigned_tx_set &exported_txs, std::vector<wallet2::pendin ptx.construction_data = sd; txs.push_back(ptx); + + // add tx keys only to ptx + txs.back().tx_key = tx_key; + txs.back().additional_tx_keys = additional_tx_keys; } // add key images @@ -6589,10 +6805,29 @@ void wallet2::get_outs(std::vector<std::vector<tools::wallet2::get_outs_entry>> error::get_output_distribution, "Decreasing offsets in rct distribution: " + std::to_string(block_offset) + ": " + std::to_string(rct_offsets[block_offset]) + ", " + std::to_string(block_offset + 1) + ": " + std::to_string(rct_offsets[block_offset + 1])); - uint64_t n_rct = rct_offsets[block_offset + 1] - rct_offsets[block_offset]; + uint64_t first_block_offset = block_offset, last_block_offset = block_offset; + for (size_t half_window = 0; half_window < GAMMA_PICK_HALF_WINDOW; ++half_window) + { + // end when we have a non empty block + uint64_t cum0 = first_block_offset > 0 ? rct_offsets[first_block_offset] - rct_offsets[first_block_offset - 1] : rct_offsets[0]; + if (cum0 > 1) + break; + uint64_t cum1 = last_block_offset > 0 ? rct_offsets[last_block_offset] - rct_offsets[last_block_offset - 1] : rct_offsets[0]; + if (cum1 > 1) + break; + if (first_block_offset == 0 && last_block_offset >= rct_offsets.size() - 2) + break; + // expand up to bounds + if (first_block_offset > 0) + --first_block_offset; + if (last_block_offset < rct_offsets.size() - 1) + ++last_block_offset; + } + const uint64_t n_rct = rct_offsets[last_block_offset] - (first_block_offset == 0 ? 0 : rct_offsets[first_block_offset - 1]); if (n_rct == 0) return rct_offsets[block_offset] ? rct_offsets[block_offset] - 1 : 0; - return rct_offsets[block_offset] + crypto::rand<uint64_t>() % n_rct; + MDEBUG("Picking 1/" << n_rct << " in " << (last_block_offset - first_block_offset + 1) << " blocks centered around " << block_offset); + return rct_offsets[first_block_offset] + crypto::rand<uint64_t>() % n_rct; }; size_t num_selected_transfers = 0; @@ -6762,6 +6997,8 @@ void wallet2::get_outs(std::vector<std::vector<tools::wallet2::get_outs_entry>> LOG_PRINT_L1("Selecting real output: " << td.m_global_output_index << " for " << print_money(amount)); } + std::unordered_map<const char*, std::set<uint64_t>> picks; + // while we still need more mixins uint64_t num_usable_outs = num_outs; bool allow_blackballed = false; @@ -6776,7 +7013,7 @@ void wallet2::get_outs(std::vector<std::vector<tools::wallet2::get_outs_entry>> // outputs, we still need to reach the minimum ring size) if (allow_blackballed) break; - MINFO("Not enough non blackballed outputs, we'll allow blackballed ones"); + MINFO("Not enough output not marked as spent, we'll allow outputs marked as spent"); allow_blackballed = true; num_usable_outs = num_outs; } @@ -6860,11 +7097,15 @@ void wallet2::get_outs(std::vector<std::vector<tools::wallet2::get_outs_entry>> } seen_indices.emplace(i); - LOG_PRINT_L2("picking " << i << " as " << type); + picks[type].insert(i); req.outputs.push_back({amount, i}); ++num_found; } + for (const auto &pick: picks) + MDEBUG("picking " << pick.first << " outputs: " << + boost::join(pick.second | boost::adaptors::transformed([](uint64_t out){return std::to_string(out);}), " ")); + // if we had enough unusable outputs, we might fall off here and still // have too few outputs, so we stuff with one to keep counts good, and // we'll error out later @@ -6880,8 +7121,15 @@ void wallet2::get_outs(std::vector<std::vector<tools::wallet2::get_outs_entry>> [](const get_outputs_out &a, const get_outputs_out &b) { return a.index < b.index; }); } - for (auto i: req.outputs) - LOG_PRINT_L1("asking for output " << i.index << " for " << print_money(i.amount)); + if (ELPP->vRegistry()->allowed(el::Level::Debug, MONERO_DEFAULT_LOG_CATEGORY)) + { + std::map<uint64_t, std::set<uint64_t>> outs; + for (const auto &i: req.outputs) + outs[i.amount].insert(i.index); + for (const auto &o: outs) + MDEBUG("asking for outputs with amount " << print_money(o.first) << ": " << + boost::join(o.second | boost::adaptors::transformed([](uint64_t out){return std::to_string(out);}), " ")); + } // get the keys for those m_daemon_rpc_mutex.lock(); @@ -10339,7 +10587,7 @@ uint64_t wallet2::import_key_images(const std::vector<std::pair<crypto::key_imag + boost::lexical_cast<std::string>(signed_key_images.size()) + ", key image " + epee::string_tools::pod_to_hex(key_image)); THROW_WALLET_EXCEPTION_IF(!crypto::check_ring_signature((const crypto::hash&)key_image, key_image, pkeys, &signature), - error::wallet_internal_error, "Signature check failed: input " + boost::lexical_cast<std::string>(n) + "/" + error::signature_check_failed, boost::lexical_cast<std::string>(n) + "/" + boost::lexical_cast<std::string>(signed_key_images.size()) + ", key image " + epee::string_tools::pod_to_hex(key_image) + ", signature " + epee::string_tools::pod_to_hex(signature) + ", pubkey " + epee::string_tools::pod_to_hex(*pkeys[0])); diff --git a/src/wallet/wallet2.h b/src/wallet/wallet2.h index 7857f36f1..680196f01 100644 --- a/src/wallet/wallet2.h +++ b/src/wallet/wallet2.h @@ -574,6 +574,14 @@ namespace tools const std::vector<crypto::secret_key> &view_keys, const std::vector<crypto::public_key> &spend_keys, uint32_t threshold); + std::string exchange_multisig_keys(const epee::wipeable_string &password, + const std::vector<std::string> &info); + /*! + * \brief Any but first round of keys exchange + */ + std::string exchange_multisig_keys(const epee::wipeable_string &password, + std::unordered_set<crypto::public_key> pkeys, + std::vector<crypto::public_key> signers); /*! * \brief Finalizes creation of a multisig wallet */ @@ -1248,6 +1256,12 @@ namespace tools bool get_rct_distribution(uint64_t &start_height, std::vector<uint64_t> &distribution); uint64_t get_segregation_fork_height() const; + void unpack_multisig_info(const std::vector<std::string>& info, + std::vector<crypto::public_key> &public_keys, + std::vector<crypto::secret_key> &secret_keys) const; + bool unpack_extra_multisig_info(const std::vector<std::string>& info, + std::vector<crypto::public_key> &signers, + std::unordered_set<crypto::public_key> &pkeys) const; void cache_tx_data(const cryptonote::transaction& tx, const crypto::hash &txid, tx_cache_data &tx_cache_data) const; @@ -1298,6 +1312,9 @@ namespace tools bool m_multisig; /*!< if > 1 spend secret key will not match spend public key */ uint32_t m_multisig_threshold; std::vector<crypto::public_key> m_multisig_signers; + //in case of general M/N multisig wallet we should perform N - M + 1 key exchange rounds and remember how many rounds are passed. + uint32_t m_multisig_rounds_passed; + std::vector<crypto::public_key> m_multisig_derivations; bool m_always_confirm_transfers; bool m_print_ring_members; bool m_store_tx_info; /*!< request txkey to be returned in RPC, and store in the wallet cache file */ diff --git a/src/wallet/wallet_errors.h b/src/wallet/wallet_errors.h index bc518d04a..b3141985d 100644 --- a/src/wallet/wallet_errors.h +++ b/src/wallet/wallet_errors.h @@ -72,6 +72,7 @@ namespace tools // tx_parse_error // get_tx_pool_error // out_of_hashchain_bounds_error + // signature_check_failed // transfer_error * // get_outs_general_error // not_enough_unlocked_money @@ -418,6 +419,14 @@ namespace tools std::string to_string() const { return refresh_error::to_string(); } }; //---------------------------------------------------------------------------------------------------- + struct signature_check_failed : public wallet_logic_error + { + explicit signature_check_failed(std::string&& loc, const std::string& message) + : wallet_logic_error(std::move(loc), "Signature check failed " + message) + { + } + }; + //---------------------------------------------------------------------------------------------------- struct transfer_error : public wallet_logic_error { protected: diff --git a/src/wallet/wallet_rpc_server.cpp b/src/wallet/wallet_rpc_server.cpp index 2b5ef3157..5e6100dfd 100644 --- a/src/wallet/wallet_rpc_server.cpp +++ b/src/wallet/wallet_rpc_server.cpp @@ -981,6 +981,8 @@ namespace tools for (auto &ptx: ptxs) { res.tx_hash_list.push_back(epee::string_tools::pod_to_hex(cryptonote::get_transaction_hash(ptx.tx))); + if (req.get_tx_keys) + res.tx_key_list.push_back(epee::string_tools::pod_to_hex(ptx.tx_key)); } if (req.export_raw) @@ -994,6 +996,171 @@ namespace tools return true; } //------------------------------------------------------------------------------------------------------------------------------ + bool wallet_rpc_server::on_describe_transfer(const wallet_rpc::COMMAND_RPC_DESCRIBE_TRANSFER::request& req, wallet_rpc::COMMAND_RPC_DESCRIBE_TRANSFER::response& res, epee::json_rpc::error& er) + { + if (!m_wallet) return not_open(er); + if (m_restricted) + { + er.code = WALLET_RPC_ERROR_CODE_DENIED; + er.message = "Command unavailable in restricted mode."; + return false; + } + if (m_wallet->key_on_device()) + { + er.code = WALLET_RPC_ERROR_CODE_UNKNOWN_ERROR; + er.message = "command not supported by HW wallet"; + return false; + } + if(m_wallet->watch_only()) + { + er.code = WALLET_RPC_ERROR_CODE_WATCH_ONLY; + er.message = "command not supported by watch-only wallet"; + return false; + } + + tools::wallet2::unsigned_tx_set exported_txs; + try + { + cryptonote::blobdata blob; + if (!epee::string_tools::parse_hexstr_to_binbuff(req.unsigned_txset, blob)) + { + er.code = WALLET_RPC_ERROR_CODE_BAD_HEX; + er.message = "Failed to parse hex."; + return false; + } + if(!m_wallet->parse_unsigned_tx_from_str(blob, exported_txs)) + { + er.code = WALLET_RPC_ERROR_CODE_BAD_UNSIGNED_TX_DATA; + er.message = "cannot load unsigned_txset"; + return false; + } + } + catch (const std::exception &e) + { + er.code = WALLET_RPC_ERROR_CODE_BAD_UNSIGNED_TX_DATA; + er.message = "failed to parse unsigned transfers: " + std::string(e.what()); + return false; + } + + std::vector<tools::wallet2::pending_tx> ptx; + try + { + // gather info to ask the user + std::unordered_map<cryptonote::account_public_address, std::pair<std::string, uint64_t>> dests; + int first_known_non_zero_change_index = -1; + for (size_t n = 0; n < exported_txs.txes.size(); ++n) + { + const tools::wallet2::tx_construction_data &cd = exported_txs.txes[n]; + res.desc.push_back({0, 0, std::numeric_limits<uint32_t>::max(), 0, {}, "", 0, "", 0, 0, ""}); + wallet_rpc::COMMAND_RPC_DESCRIBE_TRANSFER::transfer_description &desc = res.desc.back(); + + std::vector<cryptonote::tx_extra_field> tx_extra_fields; + bool has_encrypted_payment_id = false; + crypto::hash8 payment_id8 = crypto::null_hash8; + if (cryptonote::parse_tx_extra(cd.extra, tx_extra_fields)) + { + cryptonote::tx_extra_nonce extra_nonce; + if (find_tx_extra_field_by_type(tx_extra_fields, extra_nonce)) + { + crypto::hash payment_id; + if(cryptonote::get_encrypted_payment_id_from_tx_extra_nonce(extra_nonce.nonce, payment_id8)) + { + desc.payment_id = epee::string_tools::pod_to_hex(payment_id8); + has_encrypted_payment_id = true; + } + else if (cryptonote::get_payment_id_from_tx_extra_nonce(extra_nonce.nonce, payment_id)) + { + desc.payment_id = epee::string_tools::pod_to_hex(payment_id); + } + } + } + + for (size_t s = 0; s < cd.sources.size(); ++s) + { + desc.amount_in += cd.sources[s].amount; + size_t ring_size = cd.sources[s].outputs.size(); + if (ring_size < desc.ring_size) + desc.ring_size = ring_size; + } + for (size_t d = 0; d < cd.splitted_dsts.size(); ++d) + { + const cryptonote::tx_destination_entry &entry = cd.splitted_dsts[d]; + std::string address = cryptonote::get_account_address_as_str(m_wallet->nettype(), entry.is_subaddress, entry.addr); + if (has_encrypted_payment_id && !entry.is_subaddress) + address = cryptonote::get_account_integrated_address_as_str(m_wallet->nettype(), entry.addr, payment_id8); + auto i = dests.find(entry.addr); + if (i == dests.end()) + dests.insert(std::make_pair(entry.addr, std::make_pair(address, entry.amount))); + else + i->second.second += entry.amount; + desc.amount_out += entry.amount; + } + if (cd.change_dts.amount > 0) + { + auto it = dests.find(cd.change_dts.addr); + if (it == dests.end()) + { + er.code = WALLET_RPC_ERROR_CODE_BAD_UNSIGNED_TX_DATA; + er.message = "Claimed change does not go to a paid address"; + return false; + } + if (it->second.second < cd.change_dts.amount) + { + er.code = WALLET_RPC_ERROR_CODE_BAD_UNSIGNED_TX_DATA; + er.message = "Claimed change is larger than payment to the change address"; + return false; + } + if (cd.change_dts.amount > 0) + { + if (first_known_non_zero_change_index == -1) + first_known_non_zero_change_index = n; + const tools::wallet2::tx_construction_data &cdn = exported_txs.txes[first_known_non_zero_change_index]; + if (memcmp(&cd.change_dts.addr, &cdn.change_dts.addr, sizeof(cd.change_dts.addr))) + { + er.code = WALLET_RPC_ERROR_CODE_BAD_UNSIGNED_TX_DATA; + er.message = "Change goes to more than one address"; + return false; + } + } + desc.change_amount += cd.change_dts.amount; + it->second.second -= cd.change_dts.amount; + if (it->second.second == 0) + dests.erase(cd.change_dts.addr); + } + + size_t n_dummy_outputs = 0; + for (auto i = dests.begin(); i != dests.end(); ) + { + if (i->second.second > 0) + { + desc.recipients.push_back({i->second.first, i->second.second}); + } + else + ++desc.dummy_outputs; + ++i; + } + + if (desc.change_amount > 0) + { + const tools::wallet2::tx_construction_data &cd0 = exported_txs.txes[0]; + desc.change_address = get_account_address_as_str(m_wallet->nettype(), cd0.subaddr_account > 0, cd0.change_dts.addr); + } + + desc.fee = desc.amount_in - desc.amount_out; + desc.unlock_time = cd.unlock_time; + desc.extra = epee::to_hex::string({cd.extra.data(), cd.extra.size()}); + } + } + catch (const std::exception &e) + { + er.code = WALLET_RPC_ERROR_CODE_BAD_UNSIGNED_TX_DATA; + er.message = "failed to parse unsigned transfers"; + return false; + } + + return true; + } + //------------------------------------------------------------------------------------------------------------------------------ bool wallet_rpc_server::on_submit_transfer(const wallet_rpc::COMMAND_RPC_SUBMIT_TRANSFER::request& req, wallet_rpc::COMMAND_RPC_SUBMIT_TRANSFER::response& res, epee::json_rpc::error& er) { if (!m_wallet) return not_open(er); @@ -1577,6 +1744,7 @@ namespace tools epee::wipeable_string seed; if (!m_wallet->get_seed(seed)) { + er.code = WALLET_RPC_ERROR_CODE_NON_DETERMINISTIC; er.message = "The wallet is non-deterministic. Cannot display seed."; return false; } @@ -2822,8 +2990,7 @@ namespace tools { try { - m_wallet->rewrite(m_wallet->get_wallet_file(), req.new_password); - m_wallet->store(); + m_wallet->change_password(m_wallet->get_wallet_file(), req.old_password, req.new_password); LOG_PRINT_L0("Wallet password changed."); } catch (const std::exception& e) @@ -2906,6 +3073,11 @@ namespace tools er.code = WALLET_RPC_ERROR_CODE_ADDRESS_INDEX_OUT_OF_BOUNDS; er.message = e.what(); } + catch (const error::signature_check_failed& e) + { + er.code = WALLET_RPC_ERROR_CODE_WRONG_SIGNATURE; + er.message = e.what(); + } catch (const std::exception& e) { er.code = default_error_code; @@ -3125,7 +3297,7 @@ namespace tools return false; } - if (req.multisig_info.size() < threshold - 1) + if (req.multisig_info.size() < 1 || req.multisig_info.size() > total) { er.code = WALLET_RPC_ERROR_CODE_THRESHOLD_NOT_REACHED; er.message = "Needs multisig info from more participants"; @@ -3152,6 +3324,55 @@ namespace tools return true; } //------------------------------------------------------------------------------------------------------------------------------ + bool wallet_rpc_server::on_exchange_multisig_keys(const wallet_rpc::COMMAND_RPC_EXCHANGE_MULTISIG_KEYS::request& req, wallet_rpc::COMMAND_RPC_EXCHANGE_MULTISIG_KEYS::response& res, epee::json_rpc::error& er) + { + if (!m_wallet) return not_open(er); + if (m_restricted) + { + er.code = WALLET_RPC_ERROR_CODE_DENIED; + er.message = "Command unavailable in restricted mode."; + return false; + } + bool ready; + uint32_t threshold, total; + if (!m_wallet->multisig(&ready, &threshold, &total)) + { + er.code = WALLET_RPC_ERROR_CODE_NOT_MULTISIG; + er.message = "This wallet is not multisig"; + return false; + } + + if (ready) + { + er.code = WALLET_RPC_ERROR_CODE_ALREADY_MULTISIG; + er.message = "This wallet is multisig, and already finalized"; + return false; + } + + if (req.multisig_info.size() < 1 || req.multisig_info.size() > total) + { + er.code = WALLET_RPC_ERROR_CODE_THRESHOLD_NOT_REACHED; + er.message = "Needs multisig info from more participants"; + return false; + } + + try + { + res.multisig_info = m_wallet->exchange_multisig_keys(req.password, req.multisig_info); + if (res.multisig_info.empty()) + { + res.address = m_wallet->get_account().get_public_address_str(m_wallet->nettype()); + } + } + catch (const std::exception &e) + { + er.code = WALLET_RPC_ERROR_CODE_UNKNOWN_ERROR; + er.message = std::string("Error calling exchange_multisig_info: ") + e.what(); + return false; + } + return true; + } + //------------------------------------------------------------------------------------------------------------------------------ bool wallet_rpc_server::on_sign_multisig(const wallet_rpc::COMMAND_RPC_SIGN_MULTISIG::request& req, wallet_rpc::COMMAND_RPC_SIGN_MULTISIG::response& res, epee::json_rpc::error& er) { if (!m_wallet) return not_open(er); diff --git a/src/wallet/wallet_rpc_server.h b/src/wallet/wallet_rpc_server.h index ab7917a78..887723ed5 100644 --- a/src/wallet/wallet_rpc_server.h +++ b/src/wallet/wallet_rpc_server.h @@ -35,6 +35,7 @@ #include <string> #include "common/util.h" #include "net/http_server_impl_base.h" +#include "math_helper.h" #include "wallet_rpc_server_commands_defs.h" #include "wallet2.h" @@ -86,6 +87,7 @@ namespace tools MAP_JON_RPC_WE("transfer", on_transfer, wallet_rpc::COMMAND_RPC_TRANSFER) MAP_JON_RPC_WE("transfer_split", on_transfer_split, wallet_rpc::COMMAND_RPC_TRANSFER_SPLIT) MAP_JON_RPC_WE("sign_transfer", on_sign_transfer, wallet_rpc::COMMAND_RPC_SIGN_TRANSFER) + MAP_JON_RPC_WE("describe_transfer", on_describe_transfer, wallet_rpc::COMMAND_RPC_DESCRIBE_TRANSFER) MAP_JON_RPC_WE("submit_transfer", on_submit_transfer, wallet_rpc::COMMAND_RPC_SUBMIT_TRANSFER) MAP_JON_RPC_WE("sweep_dust", on_sweep_dust, wallet_rpc::COMMAND_RPC_SWEEP_DUST) MAP_JON_RPC_WE("sweep_unmixable", on_sweep_dust, wallet_rpc::COMMAND_RPC_SWEEP_DUST) @@ -141,6 +143,7 @@ namespace tools MAP_JON_RPC_WE("export_multisig_info", on_export_multisig, wallet_rpc::COMMAND_RPC_EXPORT_MULTISIG) MAP_JON_RPC_WE("import_multisig_info", on_import_multisig, wallet_rpc::COMMAND_RPC_IMPORT_MULTISIG) MAP_JON_RPC_WE("finalize_multisig", on_finalize_multisig, wallet_rpc::COMMAND_RPC_FINALIZE_MULTISIG) + MAP_JON_RPC_WE("exchange_multisig_keys", on_exchange_multisig_keys, wallet_rpc::COMMAND_RPC_EXCHANGE_MULTISIG_KEYS) MAP_JON_RPC_WE("sign_multisig", on_sign_multisig, wallet_rpc::COMMAND_RPC_SIGN_MULTISIG) MAP_JON_RPC_WE("submit_multisig", on_submit_multisig, wallet_rpc::COMMAND_RPC_SUBMIT_MULTISIG) MAP_JON_RPC_WE("get_version", on_get_version, wallet_rpc::COMMAND_RPC_GET_VERSION) @@ -165,6 +168,7 @@ namespace tools bool on_transfer(const wallet_rpc::COMMAND_RPC_TRANSFER::request& req, wallet_rpc::COMMAND_RPC_TRANSFER::response& res, epee::json_rpc::error& er); bool on_transfer_split(const wallet_rpc::COMMAND_RPC_TRANSFER_SPLIT::request& req, wallet_rpc::COMMAND_RPC_TRANSFER_SPLIT::response& res, epee::json_rpc::error& er); bool on_sign_transfer(const wallet_rpc::COMMAND_RPC_SIGN_TRANSFER::request& req, wallet_rpc::COMMAND_RPC_SIGN_TRANSFER::response& res, epee::json_rpc::error& er); + bool on_describe_transfer(const wallet_rpc::COMMAND_RPC_DESCRIBE_TRANSFER::request& req, wallet_rpc::COMMAND_RPC_DESCRIBE_TRANSFER::response& res, epee::json_rpc::error& er); bool on_submit_transfer(const wallet_rpc::COMMAND_RPC_SUBMIT_TRANSFER::request& req, wallet_rpc::COMMAND_RPC_SUBMIT_TRANSFER::response& res, epee::json_rpc::error& er); bool on_sweep_dust(const wallet_rpc::COMMAND_RPC_SWEEP_DUST::request& req, wallet_rpc::COMMAND_RPC_SWEEP_DUST::response& res, epee::json_rpc::error& er); bool on_sweep_all(const wallet_rpc::COMMAND_RPC_SWEEP_ALL::request& req, wallet_rpc::COMMAND_RPC_SWEEP_ALL::response& res, epee::json_rpc::error& er); @@ -218,6 +222,7 @@ namespace tools bool on_export_multisig(const wallet_rpc::COMMAND_RPC_EXPORT_MULTISIG::request& req, wallet_rpc::COMMAND_RPC_EXPORT_MULTISIG::response& res, epee::json_rpc::error& er); bool on_import_multisig(const wallet_rpc::COMMAND_RPC_IMPORT_MULTISIG::request& req, wallet_rpc::COMMAND_RPC_IMPORT_MULTISIG::response& res, epee::json_rpc::error& er); bool on_finalize_multisig(const wallet_rpc::COMMAND_RPC_FINALIZE_MULTISIG::request& req, wallet_rpc::COMMAND_RPC_FINALIZE_MULTISIG::response& res, epee::json_rpc::error& er); + bool on_exchange_multisig_keys(const wallet_rpc::COMMAND_RPC_EXCHANGE_MULTISIG_KEYS::request& req, wallet_rpc::COMMAND_RPC_EXCHANGE_MULTISIG_KEYS::response& res, epee::json_rpc::error& er); bool on_sign_multisig(const wallet_rpc::COMMAND_RPC_SIGN_MULTISIG::request& req, wallet_rpc::COMMAND_RPC_SIGN_MULTISIG::response& res, epee::json_rpc::error& er); bool on_submit_multisig(const wallet_rpc::COMMAND_RPC_SUBMIT_MULTISIG::request& req, wallet_rpc::COMMAND_RPC_SUBMIT_MULTISIG::response& res, epee::json_rpc::error& er); bool on_get_version(const wallet_rpc::COMMAND_RPC_GET_VERSION::request& req, wallet_rpc::COMMAND_RPC_GET_VERSION::response& res, epee::json_rpc::error& er); diff --git a/src/wallet/wallet_rpc_server_commands_defs.h b/src/wallet/wallet_rpc_server_commands_defs.h index e46745339..924f3a0f1 100644 --- a/src/wallet/wallet_rpc_server_commands_defs.h +++ b/src/wallet/wallet_rpc_server_commands_defs.h @@ -47,7 +47,7 @@ // advance which version they will stop working with // Don't go over 32767 for any of these #define WALLET_RPC_VERSION_MAJOR 1 -#define WALLET_RPC_VERSION_MINOR 4 +#define WALLET_RPC_VERSION_MINOR 5 #define MAKE_WALLET_RPC_VERSION(major,minor) (((major)<<16)|(minor)) #define WALLET_RPC_VERSION MAKE_WALLET_RPC_VERSION(WALLET_RPC_VERSION_MAJOR, WALLET_RPC_VERSION_MINOR) namespace tools @@ -531,16 +531,79 @@ namespace wallet_rpc }; }; + struct COMMAND_RPC_DESCRIBE_TRANSFER + { + struct recipient + { + std::string address; + uint64_t amount; + + BEGIN_KV_SERIALIZE_MAP() + KV_SERIALIZE(address) + KV_SERIALIZE(amount) + END_KV_SERIALIZE_MAP() + }; + + struct transfer_description + { + uint64_t amount_in; + uint64_t amount_out; + uint32_t ring_size; + uint64_t unlock_time; + std::list<recipient> recipients; + std::string payment_id; + uint64_t change_amount; + std::string change_address; + uint64_t fee; + uint32_t dummy_outputs; + std::string extra; + + BEGIN_KV_SERIALIZE_MAP() + KV_SERIALIZE(amount_in) + KV_SERIALIZE(amount_out) + KV_SERIALIZE(ring_size) + KV_SERIALIZE(unlock_time) + KV_SERIALIZE(recipients) + KV_SERIALIZE(payment_id) + KV_SERIALIZE(change_amount) + KV_SERIALIZE(change_address) + KV_SERIALIZE(fee) + KV_SERIALIZE(dummy_outputs) + KV_SERIALIZE(extra) + END_KV_SERIALIZE_MAP() + }; + + struct request + { + std::string unsigned_txset; + + BEGIN_KV_SERIALIZE_MAP() + KV_SERIALIZE(unsigned_txset) + END_KV_SERIALIZE_MAP() + }; + + struct response + { + std::list<transfer_description> desc; + + BEGIN_KV_SERIALIZE_MAP() + KV_SERIALIZE(desc) + END_KV_SERIALIZE_MAP() + }; + }; + struct COMMAND_RPC_SIGN_TRANSFER { struct request { std::string unsigned_txset; bool export_raw; + bool get_tx_keys; BEGIN_KV_SERIALIZE_MAP() KV_SERIALIZE(unsigned_txset) KV_SERIALIZE_OPT(export_raw, false) + KV_SERIALIZE_OPT(get_tx_keys, false) END_KV_SERIALIZE_MAP() }; @@ -549,11 +612,13 @@ namespace wallet_rpc std::string signed_txset; std::list<std::string> tx_hash_list; std::list<std::string> tx_raw_list; + std::list<std::string> tx_key_list; BEGIN_KV_SERIALIZE_MAP() KV_SERIALIZE(signed_txset) KV_SERIALIZE(tx_hash_list) KV_SERIALIZE(tx_raw_list) + KV_SERIALIZE(tx_key_list) END_KV_SERIALIZE_MAP() }; }; @@ -1990,6 +2055,31 @@ namespace wallet_rpc }; }; + struct COMMAND_RPC_EXCHANGE_MULTISIG_KEYS + { + struct request + { + std::string password; + std::vector<std::string> multisig_info; + + BEGIN_KV_SERIALIZE_MAP() + KV_SERIALIZE(password) + KV_SERIALIZE(multisig_info) + END_KV_SERIALIZE_MAP() + }; + + struct response + { + std::string address; + std::string multisig_info; + + BEGIN_KV_SERIALIZE_MAP() + KV_SERIALIZE(address) + KV_SERIALIZE(multisig_info) + END_KV_SERIALIZE_MAP() + }; + }; + struct COMMAND_RPC_SIGN_MULTISIG { struct request diff --git a/src/wallet/wallet_rpc_server_error_codes.h b/src/wallet/wallet_rpc_server_error_codes.h index f127ae240..9b3a2847d 100644 --- a/src/wallet/wallet_rpc_server_error_codes.h +++ b/src/wallet/wallet_rpc_server_error_codes.h @@ -73,3 +73,4 @@ #define WALLET_RPC_ERROR_CODE_BAD_SIGNED_TX_DATA -40 #define WALLET_RPC_ERROR_CODE_SIGNED_SUBMISSION -41 #define WALLET_RPC_ERROR_CODE_SIGN_UNSIGNED -42 +#define WALLET_RPC_ERROR_CODE_NON_DETERMINISTIC -43 |