diff --git a/src/gen_multisig/gen_multisig.cpp b/src/gen_multisig/gen_multisig.cpp index fd6459645..b9d71fe05 100644 --- a/src/gen_multisig/gen_multisig.cpp +++ b/src/gen_multisig/gen_multisig.cpp @@ -1,4 +1,4 @@ -// Copyright (c) 2017-2022, The Monero Project +// Copyright (c) 2017-2024, The Monero Project // // All rights reserved. // @@ -121,16 +121,15 @@ static bool generate_multisig(uint32_t threshold, uint32_t total, const std::str } // exchange keys until the wallets are done - bool ready{false}; - wallets[0]->multisig(&ready); - while (!ready) + multisig::multisig_account_status ms_status{wallets[0]->get_multisig_status()}; + while (!ms_status.is_ready) { for (size_t n = 0; n < total; ++n) { kex_msgs_intermediate[n] = wallets[n]->exchange_multisig_keys(pwd_container->password(), kex_msgs_intermediate); } - wallets[0]->multisig(&ready); + ms_status = wallets[0]->get_multisig_status(); } std::string address = wallets[0]->get_account().get_public_address_str(wallets[0]->nettype()); diff --git a/src/multisig/multisig_account.h b/src/multisig/multisig_account.h index 9cd0942d4..d1de2349e 100644 --- a/src/multisig/multisig_account.h +++ b/src/multisig/multisig_account.h @@ -1,4 +1,4 @@ -// Copyright (c) 2021-2022, The Monero Project +// Copyright (c) 2021-2024, The Monero Project // // All rights reserved. // @@ -40,6 +40,19 @@ namespace multisig { + struct multisig_account_status + { + // is the multisig account active/initialized? + bool multisig_is_active{false}; + // has the multisig account completed the main key exchange rounds? + bool kex_is_done{false}; + // is the multisig account ready to use? + bool is_ready{false}; + // multisig is: M-of-N + std::uint32_t threshold{0}; // M + std::uint32_t total{0}; // N + }; + /** * multisig account: * @@ -90,7 +103,8 @@ namespace multisig * * - prepares a kex msg for the first round of multisig key construction. * - the local account's kex msgs are signed with the base_privkey - * - the first kex msg transmits the local base_common_privkey to other participants, for creating the group's common_privkey + * - the first kex msg transmits the local base_common_privkey to other participants, for creating the group's + * common_privkey */ multisig_account(const crypto::secret_key &base_privkey, const crypto::secret_key &base_common_privkey); @@ -142,7 +156,7 @@ namespace multisig const std::string& get_next_kex_round_msg() const { return m_next_round_kex_message; } //account status functions - // account has been intialized, and the account holder can use the 'common' key + // account has been initialized, and the account holder can use the 'common' key bool account_is_active() const; // account has gone through main kex rounds, only remaining step is to verify all other participants are ready bool main_kex_rounds_done() const; @@ -177,24 +191,48 @@ namespace multisig * - If force updating with maliciously-crafted messages, the resulting account will be invalid (either unable * to complete signatures, or a 'hostage' to the malicious signer [i.e. can't sign without his participation]). */ - void kex_update(const std::vector &expanded_msgs, - const bool force_update_use_with_caution = false); + void kex_update(const std::vector &expanded_msgs, const bool force_update_use_with_caution = false); + /** + * brief: get_multisig_kex_round_booster - Create a multisig kex msg for the kex round that follows the kex round this + * account is currently working on, in order to 'boost' another participant's kex setup. + * - A booster message is for the round after the in-progress round because get_next_kex_round_msg() provides access + * to the in-progress round's message. + * - Useful for 'jumpstarting' the following kex round when you don't have messages from all other signers to complete + * the current round. + * - Sanitizes input messages and produces a new kex msg for round 'num_completed_rounds + 2'. + * + * - For example, in 2-of-3 escrowed purchasing, the [vendor, arbitrator] pair can boost the second round + * of key exchange by calling this function with the 'round 1' messages of each other. + * Then the [buyer] can use the resulting boost messages, in combination with [vender, arbitrator] round 1 messages, + * to complete the address in one step. In other words, call initialize_kex() on the round 1 messages, + * then call kex_update() on the round 2 booster messages to finish the multisig key. + * + * - Note: The 'threshold' and 'num_signers' are inputs here in case kex has not been initialized yet. + * param: threshold - threshold for multisig (M in M-of-N) + * param: num_signers - number of participants in multisig (N) + * param: expanded_msgs - set of multisig kex messages to process + * return: multisig kex message for next round + */ + multisig_kex_msg get_multisig_kex_round_booster(const std::uint32_t threshold, + const std::uint32_t num_signers, + const std::vector &expanded_msgs) const; private: // implementation of kex_update() (non-transactional) void kex_update_impl(const std::vector &expanded_msgs, const bool incomplete_signer_set); /** - * brief: initialize_kex_update - Helper for kex_update_impl() - * - Collect the local signer's shared keys to ignore in incoming messages, build the aggregate ancillary key - * if appropriate. + * brief: get_kex_exclude_pubkeys - collect the local signer's shared keys to ignore in incoming messages + * return: keys held by the local account corresponding to the 'in-progress round' + * - If 'in-progress round' is the final round, these are the local account's shares of the final aggregate key. + */ + std::vector get_kex_exclude_pubkeys() const; + /** + * brief: initialize_kex_update - initialize the multisig account for the first kex round * param: expanded_msgs - set of multisig kex messages to process * param: kex_rounds_required - number of rounds required for kex (not including post-kex verification round) - * outparam: exclude_pubkeys_out - keys held by the local account corresponding to round 'current_round' - * - If 'current_round' is the final round, these are the local account's shares of the final aggregate key. */ void initialize_kex_update(const std::vector &expanded_msgs, - const std::uint32_t kex_rounds_required, - std::vector &exclude_pubkeys_out); + const std::uint32_t kex_rounds_required); /** * brief: finalize_kex_update - Helper for kex_update_impl() * param: kex_rounds_required - number of rounds required for kex (not including post-kex verification round) diff --git a/src/multisig/multisig_account_kex_impl.cpp b/src/multisig/multisig_account_kex_impl.cpp index 243058b81..f9571bdd2 100644 --- a/src/multisig/multisig_account_kex_impl.cpp +++ b/src/multisig/multisig_account_kex_impl.cpp @@ -1,4 +1,4 @@ -// Copyright (c) 2021-2022, The Monero Project +// Copyright (c) 2021-2024, The Monero Project // // All rights reserved. // @@ -395,7 +395,7 @@ namespace multisig * - Sanitizes input msgs. * - Require uniqueness in: 'exclude_pubkeys'. * - Requires each input pubkey be recommended by 'num_recommendations = expected_round' msg signers. - * - For a final multisig key to be truly 'M-of-N', each of the the private key's components must be + * - For a final multisig key to be truly 'M-of-N', each of the private key's components must be * shared by (N - M + 1) signers. * - Requires that msgs are signed by only keys in 'signers'. * - Requires that each key in 'signers' recommends [num_signers - 2 CHOOSE (expected_round - 1)] pubkeys. @@ -567,7 +567,7 @@ namespace multisig // note: do NOT remove the local signer from the pubkey origins map, since the post-kex round can be force-updated with // just the local signer's post-kex message (if the local signer were removed, then the post-kex message's pubkeys - // would be completely lost) + // would be completely deleted) // evaluate pubkeys collected @@ -598,7 +598,7 @@ namespace multisig // note: if num_signers_required == signers.size(), then this test will ensure all signers are present in 'origins', // which contains only unique pubkeys CHECK_AND_ASSERT_THROW_MES(std::find(signers.begin(), signers.end(), origin) != signers.end(), - "An unknown origin recommended a multisig post-kex verification messsage."); + "An unknown origin recommended a multisig post-kex verification message."); } return pubkey_origins_map; @@ -608,7 +608,7 @@ namespace multisig * INTERNAL * * brief: multisig_kex_process_round_msgs - Process kex messages for the active kex round. - * - A wrapper around evaluate_multisig_kex_round_msgs() -> multisig_kex_make_next_msg(). + * - A wrapper around evaluate_multisig_kex_round_msgs() -> multisig_kex_make_round_keys(). * - In other words, evaluate the input messages and try to make a message for the next round. * - Note: Must be called on the final round's msgs to evaluate the final key components * recommended by other participants. @@ -623,7 +623,7 @@ namespace multisig * outparam: keys_to_origins_map_out - map between round keys and identity keys * - If in the final round, these are key shares recommended by other signers for the final aggregate key. * - Otherwise, these are the local account's DH derivations for the next round. - * - See multisig_kex_make_next_msg() for an explanation. + * - See multisig_kex_make_round_keys() for an explanation. * return: multisig kex message for next round, or empty message if 'current_round' is the final round */ //---------------------------------------------------------------------------------------------------------------------- @@ -684,59 +684,67 @@ namespace multisig //---------------------------------------------------------------------------------------------------------------------- // multisig_account: INTERNAL //---------------------------------------------------------------------------------------------------------------------- - void multisig_account::initialize_kex_update(const std::vector &expanded_msgs, - const std::uint32_t kex_rounds_required, - std::vector &exclude_pubkeys_out) + std::vector multisig_account::get_kex_exclude_pubkeys() const { + // exclude all keys the local account recommends + std::vector exclude_pubkeys; + if (m_kex_rounds_complete == 0) { - // the first round of kex msgs will contain each participant's base pubkeys and ancillary privkeys - - // collect participants' base common privkey shares - // note: duplicate privkeys are acceptable, and duplicates due to duplicate signers - // will be blocked by duplicate-signer errors after this function is called - std::vector participant_base_common_privkeys; - participant_base_common_privkeys.reserve(expanded_msgs.size() + 1); - - // add local ancillary base privkey - participant_base_common_privkeys.emplace_back(m_base_common_privkey); - - // add other signers' base common privkeys - for (const auto &expanded_msg : expanded_msgs) - { - if (expanded_msg.get_signing_pubkey() != m_base_pubkey) - { - participant_base_common_privkeys.emplace_back(expanded_msg.get_msg_privkey()); - } - } - - // make common privkey - make_multisig_common_privkey(std::move(participant_base_common_privkeys), m_common_privkey); - - // set common pubkey - CHECK_AND_ASSERT_THROW_MES(crypto::secret_key_to_public_key(m_common_privkey, m_common_pubkey), - "Failed to derive public key"); - - // if N-of-N, then the base privkey will be used directly to make the account's share of the final key - if (kex_rounds_required == 1) - { - m_multisig_privkeys.clear(); - m_multisig_privkeys.emplace_back(m_base_privkey); - } - - // exclude all keys the local account recommends - // - in the first round, only the local pubkey is recommended by the local signer - exclude_pubkeys_out.emplace_back(m_base_pubkey); + // in the first round, only the local pubkey is recommended by the local signer + exclude_pubkeys.emplace_back(m_base_pubkey); } else { - // in other rounds, kex msgs will contain participants' shared keys - - // ignore shared keys the account helped create for this round + // in other rounds, kex msgs will contain participants' shared keys, so ignore shared keys the account helped + // create for this round for (const auto &shared_key_with_origins : m_kex_keys_to_origins_map) - { - exclude_pubkeys_out.emplace_back(shared_key_with_origins.first); - } + exclude_pubkeys.emplace_back(shared_key_with_origins.first); + } + + return exclude_pubkeys; + } + //---------------------------------------------------------------------------------------------------------------------- + // multisig_account: INTERNAL + //---------------------------------------------------------------------------------------------------------------------- + void multisig_account::initialize_kex_update(const std::vector &expanded_msgs, + const std::uint32_t kex_rounds_required) + { + // initialization is only needed during the first round + if (m_kex_rounds_complete > 0) + return; + + // the first round of kex msgs will contain each participant's base pubkeys and ancillary privkeys, so we prepare + // them here + + // collect participants' base common privkey shares + // note: duplicate privkeys are acceptable, and duplicates due to duplicate signers + // will be blocked by duplicate-signer errors after this function is called + std::vector participant_base_common_privkeys; + participant_base_common_privkeys.reserve(expanded_msgs.size() + 1); + + // add local ancillary base privkey + participant_base_common_privkeys.emplace_back(m_base_common_privkey); + + // add other signers' base common privkeys + for (const multisig_kex_msg &expanded_msg : expanded_msgs) + { + if (expanded_msg.get_signing_pubkey() != m_base_pubkey) + participant_base_common_privkeys.emplace_back(expanded_msg.get_msg_privkey()); + } + + // make common privkey + make_multisig_common_privkey(std::move(participant_base_common_privkeys), m_common_privkey); + + // set common pubkey + CHECK_AND_ASSERT_THROW_MES(crypto::secret_key_to_public_key(m_common_privkey, m_common_pubkey), + "Failed to derive public key"); + + // if N-of-N, then the base privkey will be used directly to make the account's share of the final key + if (kex_rounds_required == 1) + { + m_multisig_privkeys.clear(); + m_multisig_privkeys.emplace_back(m_base_privkey); } } //---------------------------------------------------------------------------------------------------------------------- @@ -771,9 +779,7 @@ namespace multisig result_keys.reserve(result_keys_to_origins_map.size()); for (const auto &result_key_and_origins : result_keys_to_origins_map) - { result_keys.emplace_back(result_key_and_origins.first); - } // compute final aggregate key, update local multisig privkeys with aggregation coefficients applied m_multisig_pubkey = generate_multisig_aggregate_key(std::move(result_keys), m_multisig_privkeys); @@ -811,7 +817,8 @@ namespace multisig // derived pubkey = multisig_key * G crypto::public_key_memsafe derived_pubkey; m_multisig_privkeys.push_back( - calculate_multisig_keypair_from_derivation(derivation_and_origins.first, derived_pubkey)); + calculate_multisig_keypair_from_derivation(derivation_and_origins.first, derived_pubkey) + ); // save the account's kex key mappings for this round [derived pubkey : other signers who will have the same key] m_kex_keys_to_origins_map[derived_pubkey] = std::move(derivation_and_origins.second); @@ -863,8 +870,7 @@ namespace multisig "Multisig kex has already completed all required rounds (including post-kex verification)."); // initialize account update - std::vector exclude_pubkeys; - initialize_kex_update(expanded_msgs, kex_rounds_required, exclude_pubkeys); + this->initialize_kex_update(expanded_msgs, kex_rounds_required); // process messages into a [pubkey : {origins}] map multisig_keyset_map_memsafe_t result_keys_to_origins_map; @@ -875,12 +881,75 @@ namespace multisig m_threshold, m_signers, expanded_msgs, - exclude_pubkeys, + this->get_kex_exclude_pubkeys(), incomplete_signer_set, result_keys_to_origins_map); // finish account update - finalize_kex_update(kex_rounds_required, std::move(result_keys_to_origins_map)); + this->finalize_kex_update(kex_rounds_required, std::move(result_keys_to_origins_map)); + } + //----------------------------------------------------------------- + // multisig_account: EXTERNAL + //----------------------------------------------------------------- + multisig_kex_msg multisig_account::get_multisig_kex_round_booster(const std::uint32_t threshold, + const std::uint32_t num_signers, + const std::vector &expanded_msgs) const + { + // the messages passed in should be required for the next kex round of this account (the round it is currently + // working on) + const std::uint32_t expected_msgs_round{m_kex_rounds_complete + 1}; + const std::uint32_t kex_rounds_required{multisig_kex_rounds_required(num_signers, threshold)}; + + CHECK_AND_ASSERT_THROW_MES(num_signers > 1, "Must be at least one other multisig signer."); + CHECK_AND_ASSERT_THROW_MES(num_signers <= config::MULTISIG_MAX_SIGNERS, "Too many multisig signers specified."); + CHECK_AND_ASSERT_THROW_MES(expected_msgs_round < kex_rounds_required, + "Multisig kex booster: this account has already completed all intermediate kex rounds so it can't make a kex " + "booster (there is no round available to boost)."); + CHECK_AND_ASSERT_THROW_MES(expanded_msgs.size() > 0, "At least one input kex message expected."); + + // sanitize pubkeys from input msgs + multisig_keyset_map_memsafe_t pubkey_origins_map; + const std::uint32_t msgs_round{ + multisig_kex_msgs_sanitize_pubkeys(expanded_msgs, this->get_kex_exclude_pubkeys(), pubkey_origins_map) + }; + CHECK_AND_ASSERT_THROW_MES(msgs_round == expected_msgs_round, "Kex messages were not for expected round."); + + // remove the local signer from sanitized messages + remove_key_from_mapped_sets(m_base_pubkey, pubkey_origins_map); + + // make DH derivations for booster message + multisig_keyset_map_memsafe_t derivation_to_origins_map; + multisig_kex_make_round_keys(m_base_privkey, std::move(pubkey_origins_map), derivation_to_origins_map); + + // collect keys for booster message + std::vector next_msg_keys; + next_msg_keys.reserve(derivation_to_origins_map.size()); + + if (msgs_round + 1 == kex_rounds_required) + { + // final kex round: send DH derivation pubkeys in the message + for (const auto &derivation_and_origins : derivation_to_origins_map) + { + // multisig_privkey = H(derivation) + // derived pubkey = multisig_key * G + crypto::public_key_memsafe derived_pubkey; + calculate_multisig_keypair_from_derivation(derivation_and_origins.first, derived_pubkey); + + // save keys that should be recommended to other signers + // - The keys multisig_key*G are sent to other participants in the message, so they can be used to produce the final + // multisig key via generate_multisig_spend_public_key(). + next_msg_keys.push_back(derived_pubkey); + } + } + else //(msgs_round + 1 < kex_rounds_required) + { + // intermediate kex round: send DH derivations directly in the message + for (const auto &derivation_and_origins : derivation_to_origins_map) + next_msg_keys.push_back(derivation_and_origins.first); + } + + // produce a kex message for the round after the round this account is currently working on + return multisig_kex_msg{msgs_round + 1, m_base_privkey, std::move(next_msg_keys)}.get_msg(); } //---------------------------------------------------------------------------------------------------------------------- } //namespace multisig diff --git a/src/simplewallet/simplewallet.cpp b/src/simplewallet/simplewallet.cpp index 60bc1e930..a0ee9bd83 100644 --- a/src/simplewallet/simplewallet.cpp +++ b/src/simplewallet/simplewallet.cpp @@ -898,7 +898,6 @@ bool simple_wallet::print_seed(bool encrypted) { bool success = false; epee::wipeable_string seed; - bool ready, multisig; if (m_wallet->key_on_device()) { @@ -912,10 +911,10 @@ bool simple_wallet::print_seed(bool encrypted) } CHECK_IF_BACKGROUND_SYNCING("has no seed"); - multisig = m_wallet->multisig(&ready); - if (multisig) + const multisig::multisig_account_status ms_status{m_wallet->get_multisig_status()}; + if (ms_status.multisig_is_active) { - if (!ready) + if (!ms_status.is_ready) { fail_msg_writer() << tr("wallet is multisig but not yet finalized"); return true; @@ -924,7 +923,7 @@ bool simple_wallet::print_seed(bool encrypted) SCOPED_WALLET_UNLOCK(); - if (!multisig && !m_wallet->is_deterministic()) + if (!ms_status.multisig_is_active && !m_wallet->is_deterministic()) { fail_msg_writer() << tr("wallet is non-deterministic and has no seed"); return true; @@ -939,7 +938,7 @@ bool simple_wallet::print_seed(bool encrypted) seed_pass = pwd_container->password(); } - if (multisig) + if (ms_status.multisig_is_active) success = m_wallet->get_multisig_seed(seed, seed_pass); else if (m_wallet->is_deterministic()) success = m_wallet->get_seed(seed, seed_pass); @@ -978,7 +977,7 @@ bool simple_wallet::seed_set_language(const std::vector &args/* = s fail_msg_writer() << tr("command not supported by HW wallet"); return true; } - if (m_wallet->multisig()) + if (m_wallet->get_multisig_status().multisig_is_active) { fail_msg_writer() << tr("wallet is multisig and has no seed"); return true; @@ -1125,7 +1124,7 @@ bool simple_wallet::prepare_multisig_main(const std::vector &args, fail_msg_writer() << tr("command not supported by HW wallet"); return false; } - if (m_wallet->multisig()) + if (m_wallet->get_multisig_status().multisig_is_active) { fail_msg_writer() << tr("This wallet is already multisig"); return false; @@ -1173,7 +1172,7 @@ bool simple_wallet::make_multisig_main(const std::vector &args, boo fail_msg_writer() << tr("command not supported by HW wallet"); return false; } - if (m_wallet->multisig()) + if (m_wallet->get_multisig_status().multisig_is_active) { fail_msg_writer() << tr("This wallet is already multisig"); return false; @@ -1218,9 +1217,7 @@ bool simple_wallet::make_multisig_main(const std::vector &args, boo auto local_args = args; local_args.erase(local_args.begin()); std::string multisig_extra_info = m_wallet->make_multisig(orig_pwd_container->password(), local_args, threshold); - bool ready; - m_wallet->multisig(&ready); - if (!ready) + if (!m_wallet->get_multisig_status().is_ready) { success_msg_writer() << tr("Another step is needed"); success_msg_writer() << multisig_extra_info; @@ -1238,13 +1235,13 @@ bool simple_wallet::make_multisig_main(const std::vector &args, boo return false; } - uint32_t total; - if (!m_wallet->multisig(NULL, &threshold, &total)) + const multisig::multisig_account_status ms_status{m_wallet->get_multisig_status()}; + if (!ms_status.multisig_is_active) { fail_msg_writer() << tr("Error creating multisig: new wallet is not multisig"); return false; } - success_msg_writer() << std::to_string(threshold) << "/" << total << tr(" multisig address: ") + success_msg_writer() << std::to_string(ms_status.threshold) << "/" << ms_status.total << tr(" multisig address: ") << m_wallet->get_account().get_public_address_str(m_wallet->nettype()); return true; @@ -1270,18 +1267,18 @@ bool simple_wallet::exchange_multisig_keys_main(const std::vector & const bool force_update_use_with_caution, const bool called_by_mms) { CHECK_MULTISIG_ENABLED(); - bool ready; + const multisig::multisig_account_status ms_status{m_wallet->get_multisig_status()}; if (m_wallet->key_on_device()) { fail_msg_writer() << tr("command not supported by HW wallet"); return false; } - if (!m_wallet->multisig(&ready)) + if (!ms_status.multisig_is_active) { fail_msg_writer() << tr("This wallet is not multisig"); return false; } - if (ready) + if (ms_status.is_ready) { fail_msg_writer() << tr("This wallet is already finalized"); return false; @@ -1297,9 +1294,7 @@ bool simple_wallet::exchange_multisig_keys_main(const std::vector & try { std::string multisig_extra_info = m_wallet->exchange_multisig_keys(orig_pwd_container->password(), args, force_update_use_with_caution); - bool ready; - m_wallet->multisig(&ready); - if (!ready) + if (!m_wallet->get_multisig_status().is_ready) { message_writer() << tr("Another step is needed"); message_writer() << multisig_extra_info; @@ -1310,9 +1305,8 @@ bool simple_wallet::exchange_multisig_keys_main(const std::vector & } return true; } else { - uint32_t threshold, total; - m_wallet->multisig(NULL, &threshold, &total); - success_msg_writer() << tr("Multisig wallet has been successfully created. Current wallet type: ") << threshold << "/" << total; + const multisig::multisig_account_status ms_status_new{m_wallet->get_multisig_status()}; + success_msg_writer() << tr("Multisig wallet has been successfully created. Current wallet type: ") << ms_status_new.threshold << "/" << ms_status_new.total; success_msg_writer() << tr("Multisig address: ") << m_wallet->get_account().get_public_address_str(m_wallet->nettype()); } } @@ -1335,18 +1329,18 @@ bool simple_wallet::export_multisig(const std::vector &args) bool simple_wallet::export_multisig_main(const std::vector &args, bool called_by_mms) { CHECK_MULTISIG_ENABLED(); - bool ready; + const multisig::multisig_account_status ms_status{m_wallet->get_multisig_status()}; if (m_wallet->key_on_device()) { fail_msg_writer() << tr("command not supported by HW wallet"); return false; } - if (!m_wallet->multisig(&ready)) + if (!ms_status.multisig_is_active) { fail_msg_writer() << tr("This wallet is not multisig"); return false; } - if (!ready) + if (!ms_status.is_ready) { fail_msg_writer() << tr("This multisig wallet is not yet finalized"); return false; @@ -1402,24 +1396,24 @@ bool simple_wallet::import_multisig(const std::vector &args) bool simple_wallet::import_multisig_main(const std::vector &args, bool called_by_mms) { CHECK_MULTISIG_ENABLED(); - bool ready; - uint32_t threshold, total; + const multisig::multisig_account_status ms_status{m_wallet->get_multisig_status()}; + if (m_wallet->key_on_device()) { fail_msg_writer() << tr("command not supported by HW wallet"); return false; } - if (!m_wallet->multisig(&ready, &threshold, &total)) + if (!ms_status.multisig_is_active) { fail_msg_writer() << tr("This wallet is not multisig"); return false; } - if (!ready) + if (!ms_status.is_ready) { fail_msg_writer() << tr("This multisig wallet is not yet finalized"); return false; } - if (args.size() < threshold - 1) + if (args.size() + 1 < ms_status.threshold) { PRINT_USAGE(USAGE_IMPORT_MULTISIG_INFO); return false; @@ -1499,18 +1493,19 @@ bool simple_wallet::sign_multisig(const std::vector &args) bool simple_wallet::sign_multisig_main(const std::vector &args, bool called_by_mms) { CHECK_MULTISIG_ENABLED(); - bool ready; + const multisig::multisig_account_status ms_status{m_wallet->get_multisig_status()};\ + if (m_wallet->key_on_device()) { fail_msg_writer() << tr("command not supported by HW wallet"); return false; } - if(!m_wallet->multisig(&ready)) + if (!ms_status.multisig_is_active) { fail_msg_writer() << tr("This is not a multisig wallet"); return false; } - if (!ready) + if (!ms_status.is_ready) { fail_msg_writer() << tr("This multisig wallet is not yet finalized"); return false; @@ -1584,9 +1579,7 @@ bool simple_wallet::sign_multisig_main(const std::vector &args, boo if (txids.empty()) { - uint32_t threshold; - m_wallet->multisig(NULL, &threshold); - uint32_t signers_needed = threshold - signers - 1; + uint32_t signers_needed = ms_status.threshold - signers - 1; success_msg_writer(true) << tr("Transaction successfully signed to file ") << filename << ", " << signers_needed << " more signer(s) needed"; return true; @@ -1616,19 +1609,19 @@ bool simple_wallet::submit_multisig(const std::vector &args) bool simple_wallet::submit_multisig_main(const std::vector &args, bool called_by_mms) { CHECK_MULTISIG_ENABLED(); - bool ready; - uint32_t threshold; + const multisig::multisig_account_status ms_status{m_wallet->get_multisig_status()}; + if (m_wallet->key_on_device()) { fail_msg_writer() << tr("command not supported by HW wallet"); return false; } - if (!m_wallet->multisig(&ready, &threshold)) + if (!ms_status.multisig_is_active) { fail_msg_writer() << tr("This is not a multisig wallet"); return false; } - if (!ready) + if (!ms_status.is_ready) { fail_msg_writer() << tr("This multisig wallet is not yet finalized"); return false; @@ -1666,10 +1659,10 @@ bool simple_wallet::submit_multisig_main(const std::vector &args, b return false; } } - if (txs.m_signers.size() < threshold) + if (txs.m_signers.size() < ms_status.threshold) { fail_msg_writer() << (boost::format(tr("Multisig transaction signed by only %u signers, needs %u more signatures")) - % txs.m_signers.size() % (threshold - txs.m_signers.size())).str(); + % txs.m_signers.size() % (ms_status.threshold - txs.m_signers.size())).str(); return false; } @@ -1698,19 +1691,19 @@ bool simple_wallet::submit_multisig_main(const std::vector &args, b bool simple_wallet::export_raw_multisig(const std::vector &args) { CHECK_MULTISIG_ENABLED(); - bool ready; - uint32_t threshold; + const multisig::multisig_account_status ms_status{m_wallet->get_multisig_status()}; + if (m_wallet->key_on_device()) { fail_msg_writer() << tr("command not supported by HW wallet"); return true; } - if (!m_wallet->multisig(&ready, &threshold)) + if (!ms_status.multisig_is_active) { fail_msg_writer() << tr("This is not a multisig wallet"); return true; } - if (!ready) + if (!ms_status.is_ready) { fail_msg_writer() << tr("This multisig wallet is not yet finalized"); return true; @@ -1736,10 +1729,10 @@ bool simple_wallet::export_raw_multisig(const std::vector &args) fail_msg_writer() << tr("Failed to load multisig transaction from file"); return true; } - if (txs.m_signers.size() < threshold) + if (txs.m_signers.size() < ms_status.threshold) { fail_msg_writer() << (boost::format(tr("Multisig transaction signed by only %u signers, needs %u more signatures")) - % txs.m_signers.size() % (threshold - txs.m_signers.size())).str(); + % txs.m_signers.size() % (ms_status.threshold - txs.m_signers.size())).str(); return true; } @@ -3082,7 +3075,7 @@ bool simple_wallet::set_track_uses(const std::vector &args/* = std: bool simple_wallet::setup_background_sync(const std::vector &args/* = std::vector()*/) { - if (m_wallet->multisig()) + if (m_wallet->get_multisig_status().multisig_is_active) { fail_msg_writer() << tr("background sync not implemented for multisig wallet"); return true; @@ -4311,7 +4304,7 @@ void simple_wallet::print_seed(const epee::wipeable_string &seed) { success_msg_writer(true) << "\n" << boost::format(tr("NOTE: the following %s can be used to recover access to your wallet. " "Write them down and store them somewhere safe and secure. Please do not store them in " - "your email or on file storage services outside of your immediate control.\n")) % (m_wallet->multisig() ? tr("string") : tr("25 words")); + "your email or on file storage services outside of your immediate control.\n")) % (m_wallet->get_multisig_status().multisig_is_active ? tr("string") : tr("25 words")); // don't log int space_index = 0; size_t len = seed.size(); @@ -5332,7 +5325,7 @@ boost::optional simple_wallet::new_wallet(const boost::pr } //---------------------------------------------------------------------------------------------------- boost::optional simple_wallet::new_wallet(const boost::program_options::variables_map& vm, - const epee::wipeable_string &multisig_keys, const epee::wipeable_string &seed_pass, const std::string &old_language) + const epee::wipeable_string &multisig_keys, const epee::wipeable_string &seed_pass, const std::string &old_language) { std::pair, tools::password_container> rc; try { rc = tools::wallet2::make_new(vm, false, password_prompter); } @@ -5376,14 +5369,14 @@ boost::optional simple_wallet::new_wallet(const boost::pr const epee::wipeable_string &msig_keys = m_wallet->decrypt(std::string(multisig_keys.data(), multisig_keys.size()), key, true); m_wallet->generate(m_wallet_file, std::move(rc.second).password(), msig_keys, create_address_file); } - bool ready; - uint32_t threshold, total; - if (!m_wallet->multisig(&ready, &threshold, &total) || !ready) + const multisig::multisig_account_status ms_status{m_wallet->get_multisig_status()}; + + if (!ms_status.multisig_is_active || !ms_status.is_ready) { fail_msg_writer() << tr("failed to generate new mutlisig wallet"); return {}; } - message_writer(console_color_white, true) << boost::format(tr("Generated new %u/%u multisig wallet: ")) % threshold % total + message_writer(console_color_white, true) << boost::format(tr("Generated new %u/%u multisig wallet: ")) % ms_status.threshold % ms_status.total << m_wallet->get_account().get_public_address_str(m_wallet->nettype()); } catch (const std::exception& e) @@ -5427,12 +5420,11 @@ boost::optional simple_wallet::open_wallet(const boost::p m_wallet->callback(this); m_wallet->load(m_wallet_file, password); std::string prefix; - bool ready; - uint32_t threshold, total; + const multisig::multisig_account_status ms_status{m_wallet->get_multisig_status()}; if (m_wallet->watch_only()) prefix = tr("Opened watch-only wallet"); - else if (m_wallet->multisig(&ready, &threshold, &total)) - prefix = (boost::format(tr("Opened %u/%u multisig wallet%s")) % threshold % total % (ready ? "" : " (not yet finalized)")).str(); + else if (ms_status.multisig_is_active) + prefix = (boost::format(tr("Opened %u/%u multisig wallet%s")) % ms_status.threshold % ms_status.total % (ms_status.is_ready ? "" : " (not yet finalized)")).str(); else if (m_wallet->is_background_wallet()) prefix = tr("Opened background wallet"); else @@ -5553,7 +5545,7 @@ bool simple_wallet::save(const std::vector &args) //---------------------------------------------------------------------------------------------------- bool simple_wallet::save_watch_only(const std::vector &args/* = std::vector()*/) { - if (m_wallet->multisig()) + if (m_wallet->get_multisig_status().multisig_is_active) { fail_msg_writer() << tr("wallet is multisig and cannot save a watch-only version"); return true; @@ -7362,7 +7354,7 @@ bool simple_wallet::transfer_main( } // actually commit the transactions - if (m_wallet->multisig() && called_by_mms) + if (m_wallet->get_multisig_status().multisig_is_active && called_by_mms) { std::string ciphertext = m_wallet->save_multisig_tx(ptx_vector); if (!ciphertext.empty()) @@ -7371,7 +7363,7 @@ bool simple_wallet::transfer_main( success_msg_writer(true) << tr("Unsigned transaction(s) successfully written to MMS"); } } - else if (m_wallet->multisig()) + else if (m_wallet->get_multisig_status().multisig_is_active) { bool r = m_wallet->save_multisig_tx(ptx_vector, "multisig_salvium_tx"); if (!r) @@ -7579,7 +7571,7 @@ bool simple_wallet::sweep_unmixable(const std::vector &args_) } // actually commit the transactions - if (m_wallet->multisig()) + if (m_wallet->get_multisig_status().multisig_is_active) { CHECK_MULTISIG_ENABLED(); bool r = m_wallet->save_multisig_tx(ptx_vector, "multisig_salvium_tx"); @@ -7888,7 +7880,7 @@ bool simple_wallet::sweep_main(uint32_t account, uint64_t below, bool locked, co } // actually commit the transactions - if (m_wallet->multisig()) + if (m_wallet->get_multisig_status().multisig_is_active) { CHECK_MULTISIG_ENABLED(); bool r = m_wallet->save_multisig_tx(ptx_vector, "multisig_salvium_tx"); @@ -8124,7 +8116,7 @@ bool simple_wallet::sweep_single(const std::vector &args_) } // actually commit the transactions - if (m_wallet->multisig()) + if (m_wallet->get_multisig_status().multisig_is_active) { CHECK_MULTISIG_ENABLED(); bool r = m_wallet->save_multisig_tx(ptx_vector, "multisig_salvium_tx"); @@ -8375,7 +8367,7 @@ bool simple_wallet::return_payment(const std::vector &args_) } // actually commit the transactions - if (m_wallet->multisig()) + if (m_wallet->get_multisig_status().multisig_is_active) { CHECK_MULTISIG_ENABLED(); bool r = m_wallet->save_multisig_tx(ptx_vector, "multisig_salvium_tx"); @@ -8577,7 +8569,7 @@ bool simple_wallet::audit(const std::vector &args_) return true; } - if(m_wallet->multisig()) + if(m_wallet->get_multisig_status().multisig_is_active) { fail_msg_writer() << tr("This is a multisig wallet, staking is not currently supported"); return true; @@ -8606,7 +8598,7 @@ bool simple_wallet::stake(const std::vector &args_) return true; } - if(m_wallet->multisig()) + if(m_wallet->get_multisig_status().multisig_is_active) { fail_msg_writer() << tr("This is a multisig wallet, staking is not currently supported"); return true; @@ -8994,7 +8986,7 @@ bool simple_wallet::sign_transfer(const std::vector &args_) fail_msg_writer() << tr("command not supported by HW wallet"); return true; } - if(m_wallet->multisig()) + if(m_wallet->get_multisig_status().multisig_is_active) { fail_msg_writer() << tr("This is a multisig wallet, it can only sign with sign_multisig"); return true; @@ -9536,7 +9528,7 @@ bool simple_wallet::get_reserve_proof(const std::vector &args) return true; } - if (m_wallet->watch_only() || m_wallet->multisig()) + if (m_wallet->watch_only() || m_wallet->get_multisig_status().multisig_is_active) { fail_msg_writer() << tr("The reserve proof can be generated only by a full wallet"); return true; @@ -11160,8 +11152,8 @@ bool simple_wallet::status(const std::vector &args) //---------------------------------------------------------------------------------------------------- bool simple_wallet::wallet_info(const std::vector &args) { - bool ready; - uint32_t threshold, total; + const multisig::multisig_account_status ms_status{m_wallet->get_multisig_status()}; + std::string description = m_wallet->get_description(); if (description.empty()) { @@ -11173,8 +11165,8 @@ bool simple_wallet::wallet_info(const std::vector &args) std::string type; if (m_wallet->watch_only()) type = tr("Watch only"); - else if (m_wallet->multisig(&ready, &threshold, &total)) - type = (boost::format(tr("%u/%u multisig%s")) % threshold % total % (ready ? "" : " (not yet finalized)")).str(); + else if (ms_status.multisig_is_active) + type = (boost::format(tr("%u/%u multisig%s")) % ms_status.threshold % ms_status.total % (ms_status.is_ready ? "" : " (not yet finalized)")).str(); else if (m_wallet->is_background_wallet()) type = tr("Background wallet"); else @@ -11204,7 +11196,7 @@ bool simple_wallet::sign(const std::vector &args) fail_msg_writer() << tr("wallet is watch-only and cannot sign"); return true; } - if (m_wallet->multisig()) + if (m_wallet->get_multisig_status().multisig_is_active) { fail_msg_writer() << tr("This wallet is multisig and cannot sign"); return true; diff --git a/src/wallet/message_store.h b/src/wallet/message_store.h index c5421a702..17f8fb698 100644 --- a/src/wallet/message_store.h +++ b/src/wallet/message_store.h @@ -245,18 +245,23 @@ namespace mms crypto::secret_key view_secret_key; bool multisig; bool multisig_is_ready; + bool multisig_kex_is_done; bool has_multisig_partial_key_images; uint32_t multisig_rounds_passed; size_t num_transfer_details; std::string mms_file; BEGIN_SERIALIZE_OBJECT() - VERSION_FIELD(0) + VERSION_FIELD(1) FIELD(address) VARINT_FIELD(nettype) FIELD(view_secret_key) FIELD(multisig) FIELD(multisig_is_ready) + if (version > 0) + FIELD(multisig_kex_is_done) + else + multisig_kex_is_done = multisig_is_ready; FIELD(has_multisig_partial_key_images) VARINT_FIELD(multisig_rounds_passed) VARINT_FIELD(num_transfer_details) diff --git a/src/wallet/wallet2.cpp b/src/wallet/wallet2.cpp index b4ea6ce28..a18fde5e9 100644 --- a/src/wallet/wallet2.cpp +++ b/src/wallet/wallet2.cpp @@ -1102,27 +1102,29 @@ boost::mutex wallet_keys_unlocker::lockers_lock; std::map wallet_keys_unlocker::lockers_per_wallet = {}; wallet_keys_unlocker::wallet_keys_unlocker(wallet2 &w, const epee::wipeable_string *password): w(w), - can_relock(true) + should_relock(true) { static_assert(!std::is_move_assignable_v && !std::is_move_constructible_v, "Keeping track of wallet instances by pointer doesn't work if wallet2 can be moved"); - if (password == nullptr || !w.is_spendkey_encryption_enabled()) + if (password == nullptr || !w.is_key_encryption_enabled()) { - can_relock = false; + should_relock = false; return; } + w.generate_chacha_key_from_password(*password, key); + boost::lock_guard lock(lockers_lock); if (lockers_per_wallet[std::addressof(w)]++ > 0) return; - w.generate_chacha_key_from_password(*password, key); + w.decrypt_keys(key); } wallet_keys_unlocker::~wallet_keys_unlocker() { - if (!can_relock) + if (!should_relock) return; try @@ -1454,20 +1456,20 @@ bool wallet2::get_seed(epee::wipeable_string& electrum_words, const epee::wipeab //---------------------------------------------------------------------------------------------------- bool wallet2::get_multisig_seed(epee::wipeable_string& seed, const epee::wipeable_string &passphrase) const { - bool ready; - uint32_t threshold, total; - if (!multisig(&ready, &threshold, &total)) + const multisig::multisig_account_status ms_status{get_multisig_status()}; + + if (!ms_status.multisig_is_active) { std::cout << "This is not a multisig wallet" << std::endl; return false; } - if (!ready) + if (!ms_status.is_ready) { std::cout << "This multisig wallet is not yet finalized" << std::endl; return false; } - const uint64_t num_expected_ms_keys = num_priv_multisig_keys_post_setup(threshold, total); + const uint64_t num_expected_ms_keys = num_priv_multisig_keys_post_setup(ms_status.threshold, ms_status.total); crypto::secret_key skey; crypto::public_key pkey; @@ -1475,8 +1477,8 @@ bool wallet2::get_multisig_seed(epee::wipeable_string& seed, const epee::wipeabl THROW_WALLET_EXCEPTION_IF(num_expected_ms_keys != keys.m_multisig_keys.size(), error::wallet_internal_error, "Unexpected number of private multisig keys") epee::wipeable_string data; - data.append((const char*)&threshold, sizeof(uint32_t)); - data.append((const char*)&total, sizeof(uint32_t)); + data.append((const char*)&ms_status.threshold, sizeof(uint32_t)); + data.append((const char*)&ms_status.total, sizeof(uint32_t)); skey = keys.m_spend_secret_key; data.append((const char*)&skey, sizeof(skey)); pkey = keys.m_account_address.m_spend_public_key; @@ -2386,7 +2388,7 @@ void wallet2::scan_key_image(const wallet::enote_view_incoming_scan_info_t &enot return; // if keys are encrypted, ask for password - if (is_spendkey_encryption_enabled()) + if (is_key_encryption_enabled()) { static critical_section password_lock; CRITICAL_REGION_LOCAL(password_lock); @@ -5337,6 +5339,14 @@ bool wallet2::verify_password(const std::string& keys_file_name, const epee::wip return r; } +bool wallet2::is_key_encryption_enabled() const +{ + return !is_unattended() + && ask_password() == tools::wallet2::AskPasswordToDecrypt + && !watch_only() + && !is_background_syncing(); +} + void wallet2::encrypt_keys(const crypto::chacha_key &key) { m_account.encrypt_keys(key); @@ -5737,26 +5747,52 @@ void wallet2::restore(const std::string& wallet_, const epee::wipeable_string& p } } //---------------------------------------------------------------------------------------------------- +void wallet2::get_uninitialized_multisig_account(multisig::multisig_account &account_out) const +{ + // create uninitialized multisig account + account_out = multisig::multisig_account{ + // k_base = H(normal private spend key) + multisig::get_multisig_blinded_secret_key(this->get_account().get_keys().m_spend_secret_key), + // k_view = H(normal private view key) + multisig::get_multisig_blinded_secret_key(this->get_account().get_keys().m_view_secret_key) + }; +} +//---------------------------------------------------------------------------------------------------- +void wallet2::get_reconstructed_multisig_account(multisig::multisig_account &account_out) const +{ + const multisig::multisig_account_status ms_status{this->get_multisig_status()}; + CHECK_AND_ASSERT_THROW_MES(ms_status.multisig_is_active, + "The wallet is not multisig, so the multisig account couldn't be reconstructed"); + + // reconstruct multisig account + crypto::public_key common_pubkey; + crypto::secret_key_to_public_key(this->get_account().get_keys().m_view_secret_key, common_pubkey); + + multisig::multisig_keyset_map_memsafe_t kex_origins_map; + for (const auto &derivation : m_multisig_derivations) + kex_origins_map[derivation]; + + account_out = multisig::multisig_account{ + m_multisig_threshold, + m_multisig_signers, + this->get_account().get_keys().m_spend_secret_key, + this->get_account().get_keys().m_view_secret_key, + this->get_account().get_keys().m_multisig_keys, + this->get_account().get_keys().m_view_secret_key, + m_account_public_address.m_spend_public_key, + common_pubkey, + m_multisig_rounds_passed, + std::move(kex_origins_map), + "" + }; +} +//---------------------------------------------------------------------------------------------------- std::string wallet2::make_multisig(const epee::wipeable_string &password, const std::vector &initial_kex_msgs, const std::uint32_t threshold) { // decrypt account keys - 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); - } - ); - } + std::optional unlocker(std::in_place, *this, &password); // create multisig account multisig::multisig_account multisig_account{ @@ -5833,7 +5869,7 @@ std::string wallet2::make_multisig(const epee::wipeable_string &password, m_account_public_address.m_spend_public_key = multisig_account.get_multisig_pubkey(); // re-encrypt keys - keys_reencryptor = epee::misc_utils::auto_scope_leave_caller(); + unlocker.reset(); if (!m_wallet_file.empty()) create_keys_file(m_wallet_file, false, password, boost::filesystem::exists(m_wallet_file + ".address.txt")); @@ -5850,45 +5886,15 @@ std::string wallet2::exchange_multisig_keys(const epee::wipeable_string &passwor const std::vector &kex_messages, const bool force_update_use_with_caution /*= false*/) { - bool ready{false}; - CHECK_AND_ASSERT_THROW_MES(multisig(&ready), "The wallet is not multisig"); + const multisig::multisig_account_status ms_status{this->get_multisig_status()}; + CHECK_AND_ASSERT_THROW_MES(ms_status.multisig_is_active, "The wallet is not multisig"); // decrypt account keys - 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); - } - ); - } + std::optional unlocker(std::in_place, *this, &password); // reconstruct multisig account - multisig::multisig_keyset_map_memsafe_t kex_origins_map; - - for (const auto &derivation : m_multisig_derivations) - kex_origins_map[derivation]; - - multisig::multisig_account multisig_account{ - m_multisig_threshold, - m_multisig_signers, - get_account().get_keys().m_spend_secret_key, - crypto::null_skey, //base common privkey: not used - get_account().get_keys().m_multisig_keys, - get_account().get_keys().m_view_secret_key, - m_account_public_address.m_spend_public_key, - m_account_public_address.m_view_public_key, - m_multisig_rounds_passed, - std::move(kex_origins_map), - "" - }; + multisig::multisig_account multisig_account; + this->get_reconstructed_multisig_account(multisig_account); // KLUDGE: early return if there are no kex messages and main kex is complete (will return the post-kex verification round // message) (it's a kludge because this behavior would be more appropriate for a standalone wallet method) @@ -5904,7 +5910,7 @@ std::string wallet2::exchange_multisig_keys(const epee::wipeable_string &passwor std::vector expanded_msgs; expanded_msgs.reserve(kex_messages.size()); - for (const auto &msg : kex_messages) + for (const std::string &msg : kex_messages) expanded_msgs.emplace_back(msg); // update multisig kex @@ -5936,31 +5942,31 @@ std::string wallet2::exchange_multisig_keys(const epee::wipeable_string &passwor if (multisig_account.multisig_is_ready()) { // keys are encrypted again - keys_reencryptor = epee::misc_utils::auto_scope_leave_caller(); + unlocker.reset(); if (!m_wallet_file.empty()) { - bool r = store_keys(m_keys_file, password, false); + bool r = this->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 = save_to_file(m_wallet_file + ".address.txt", m_account.get_public_address_str(m_nettype), true); + r = this->save_to_file(m_wallet_file + ".address.txt", m_account.get_public_address_str(m_nettype), true); if(!r) MERROR("String with address text not saved"); } } m_subaddresses.clear(); m_subaddress_labels.clear(); - add_subaddress_account(tr("Primary account")); + this->add_subaddress_account(tr("Primary account")); if (!m_wallet_file.empty()) - store(); + this->store(); } // wallet/file relationship if (!m_wallet_file.empty()) - create_keys_file(m_wallet_file, false, password, boost::filesystem::exists(m_wallet_file + ".address.txt")); + this->create_keys_file(m_wallet_file, false, password, boost::filesystem::exists(m_wallet_file + ".address.txt")); return multisig_account.get_next_kex_round_msg(); } @@ -5978,20 +5984,81 @@ std::string wallet2::get_multisig_first_kex_msg() const return multisig_account.get_next_kex_round_msg(); } //---------------------------------------------------------------------------------------------------- -bool wallet2::multisig(bool *ready, uint32_t *threshold, uint32_t *total) const +std::string wallet2::get_multisig_key_exchange_booster(const epee::wipeable_string &password, + const std::vector &kex_messages, + const std::uint32_t threshold, + const std::uint32_t num_signers) { - if (!m_multisig) - return false; - if (threshold) - *threshold = m_multisig_threshold; - if (total) - *total = m_multisig_signers.size(); - if (ready) + CHECK_AND_ASSERT_THROW_MES(kex_messages.size() > 0, "No key exchange messages passed in."); + + // decrypt account keys + wallet_keys_unlocker unlocker(*this, &password); + + // prepare multisig account + multisig::multisig_account multisig_account; + + const multisig::multisig_account_status ms_status{this->get_multisig_status()}; + CHECK_AND_ASSERT_THROW_MES(!ms_status.is_ready, "Multisig wallet creation process has already been finished."); + + if (ms_status.multisig_is_active) { - *ready = !(get_account().get_keys().m_account_address.m_spend_public_key == rct::rct2pk(rct::identity())) && + // case: this wallet is in the middle of multisig key exchange + // - boost the round that comes after the in-progress round + + CHECK_AND_ASSERT_THROW_MES(threshold == m_multisig_threshold, + "Expected threshold does not match multisig wallet setting."); + CHECK_AND_ASSERT_THROW_MES(num_signers == m_multisig_signers.size(), + "Expected number of signers does not match multisig wallet setting."); + + // reconstruct multisig account + this->get_reconstructed_multisig_account(multisig_account); + } + else + { + // case: make_multisig() has not been called + // DANGER: If 'num_signers - threshold > 1', but this wallet's future multisig settings + // will be 'num_signers - threshold == 1', then the booster message WILL leak the + // future multisig wallet's private keys in this case where the wallet2 multisig wallet is uninitialized. + + this->get_uninitialized_multisig_account(multisig_account); + } + + // open kex messages + std::vector expanded_msgs; + expanded_msgs.reserve(kex_messages.size()); + + for (const std::string &msg : kex_messages) + expanded_msgs.emplace_back(msg); + + // get kex booster message + // note: booster does not change wallet state other than decrypting/reencrypting account keys + return multisig_account.get_multisig_kex_round_booster(threshold, num_signers, expanded_msgs).get_msg(); +} +//---------------------------------------------------------------------------------------------------- +multisig::multisig_account_status wallet2::get_multisig_status() const +{ + multisig::multisig_account_status ret; + + if (m_multisig) + { + ret.multisig_is_active = true; + ret.threshold = m_multisig_threshold; + ret.total = m_multisig_signers.size(); + ret.kex_is_done = !(get_account().get_keys().m_account_address.m_spend_public_key == rct::rct2pk(rct::identity())) && + (m_multisig_rounds_passed >= multisig::multisig_kex_rounds_required(m_multisig_signers.size(), m_multisig_threshold)); + ret.is_ready = ret.kex_is_done && (m_multisig_rounds_passed == multisig::multisig_setup_rounds_required(m_multisig_signers.size(), m_multisig_threshold)); } - return true; + else + { + ret.multisig_is_active = false; + ret.threshold = 0; + ret.total = 0; + ret.kex_is_done = false; + ret.is_ready = false; + } + + return ret; } //---------------------------------------------------------------------------------------------------- bool wallet2::has_multisig_partial_key_images() const @@ -15481,15 +15548,19 @@ void wallet2::generate_genesis(cryptonote::block& b) const { //---------------------------------------------------------------------------------------------------- mms::multisig_wallet_state wallet2::get_multisig_wallet_state() const { + const multisig::multisig_account_status ms_status{get_multisig_status()}; + mms::multisig_wallet_state state; state.nettype = m_nettype; - state.multisig = multisig(&state.multisig_is_ready); + state.multisig = ms_status.multisig_is_active; + state.multisig_is_ready = ms_status.is_ready; + state.multisig_kex_is_done = ms_status.kex_is_done; state.has_multisig_partial_key_images = has_multisig_partial_key_images(); state.multisig_rounds_passed = m_multisig_rounds_passed; state.num_transfer_details = m_transfers.size(); if (state.multisig) { - THROW_WALLET_EXCEPTION_IF(!m_original_keys_available, error::wallet_internal_error, "MMS use not possible because own original Salvium address not available"); + THROW_WALLET_EXCEPTION_IF(!m_original_keys_available, error::wallet_internal_error, "MMS use not possible because own original Monero address not available"); state.address = m_original_address; state.view_secret_key = m_original_view_secret_key; } diff --git a/src/wallet/wallet2.h b/src/wallet/wallet2.h index 2933d441c..f86d312f8 100644 --- a/src/wallet/wallet2.h +++ b/src/wallet/wallet2.h @@ -58,6 +58,7 @@ #include "common/util.h" #include "crypto/chacha.h" #include "crypto/hash.h" +#include "multisig/multisig_account.h" #include "ringct/rctTypes.h" #include "ringct/rctOps.h" #include "checkpoints/checkpoints.h" @@ -90,6 +91,7 @@ class Serialization_portability_wallet_Test; class wallet_accessor_test; +namespace multisig { class multisig_account; } namespace tools { @@ -129,7 +131,7 @@ private: ~wallet_keys_unlocker(); private: wallet2 &w; - bool can_relock; + bool should_relock; crypto::chacha_key key; static boost::mutex lockers_lock; static std::map lockers_per_wallet; @@ -981,6 +983,18 @@ private: */ void restore(const std::string& wallet_, const epee::wipeable_string& password, const std::string &device_name, bool create_address_file = false); + private: + /*! + * \brief Creates an uninitialized multisig account + * \outparam: the uninitialized multisig account + */ + void get_uninitialized_multisig_account(multisig::multisig_account &account_out) const; + /*! + * \brief Reconstructs a multisig account from wallet2 state + * \outparam: the reconstructed multisig account + */ + void get_reconstructed_multisig_account(multisig::multisig_account &account_out) const; + public: /*! * \brief Creates a multisig wallet * \return empty if done, non empty if we need to send another string @@ -1002,6 +1016,13 @@ private: * \return string to send to other participants */ std::string get_multisig_first_kex_msg() const; + /*! + * \brief Use multisig kex messages for an in-progress kex round to 'boost' the following round for another group member + */ + std::string get_multisig_key_exchange_booster(const epee::wipeable_string &password, + const std::vector &kex_messages, + const std::uint32_t threshold, + const std::uint32_t num_signers); /*! * Export multisig info * This will generate and remember new k values @@ -1067,6 +1088,7 @@ private: cryptonote::account_base& get_account(){return m_account;} const cryptonote::account_base& get_account()const{return m_account;} + bool is_key_encryption_enabled() const; void encrypt_keys(const crypto::chacha_key &key); void encrypt_keys(const epee::wipeable_string &password); void decrypt_keys(const crypto::chacha_key &key); @@ -1150,8 +1172,8 @@ private: cryptonote::network_type nettype() const { return m_nettype; } bool watch_only() const { return m_watch_only; } - bool multisig(bool *ready = NULL, uint32_t *threshold = NULL, uint32_t *total = NULL) const; bool is_background_wallet() const { return m_is_background_wallet; } + multisig::multisig_account_status get_multisig_status() const; bool has_multisig_partial_key_images() const; bool has_unknown_key_images() const; bool get_multisig_seed(epee::wipeable_string& seed, const epee::wipeable_string &passphrase = std::string()) const; @@ -1706,8 +1728,6 @@ private: uint32_t adjust_priority(uint32_t priority); bool is_unattended() const { return m_unattended; } - bool is_spendkey_encryption_enabled() const - { return m_ask_password == AskPasswordToDecrypt && !m_unattended && !m_watch_only && !m_multisig && !m_is_background_wallet; } std::pair estimate_tx_size_and_weight(bool use_rct, int n_inputs, int ring_size, int n_outputs, size_t extra_size); diff --git a/src/wallet/wallet_rpc_server.cpp b/src/wallet/wallet_rpc_server.cpp index 58312f06b..adb9c9c83 100644 --- a/src/wallet/wallet_rpc_server.cpp +++ b/src/wallet/wallet_rpc_server.cpp @@ -65,7 +65,7 @@ using namespace epee; #define CHECK_MULTISIG_ENABLED() \ do \ { \ - if (m_wallet->multisig() && !m_wallet->is_multisig_enabled()) \ + if (m_wallet->get_multisig_status().multisig_is_active && !m_wallet->is_multisig_enabled()) \ { \ er.code = WALLET_RPC_ERROR_CODE_DISABLED; \ er.message = "This wallet is multisig, and multisig is disabled. Multisig is an experimental feature and may have bugs. Things that could go wrong include: funds sent to a multisig wallet can't be spent at all, can only be spent with the participation of a malicious group member, or can be stolen by a malicious group member. You can enable it by running this once in salvium-wallet-cli: set enable-multisig-experimental 1"; \ @@ -107,7 +107,7 @@ using namespace epee; er.message = "Command not supported by HW wallet"; \ return false; \ } \ - if (m_wallet->multisig()) \ + if (m_wallet->get_multisig_status().multisig_is_active) \ { \ er.code = WALLET_RPC_ERROR_CODE_UNKNOWN_ERROR; \ er.message = "Multisig wallet cannot enable background sync"; \ @@ -551,7 +551,7 @@ namespace tools if (!balance_info.balance) continue; balance_info.unlocked_balance = req.all_accounts ? m_wallet->unlocked_balance_all(req.strict, asset, &balance_info.blocks_to_unlock, &balance_info.time_to_unlock) : m_wallet->unlocked_balance(req.account_index, asset, req.strict, &balance_info.blocks_to_unlock, &balance_info.time_to_unlock); - balance_info.multisig_import_needed = m_wallet->multisig() && m_wallet->has_multisig_partial_key_images(); + balance_info.multisig_import_needed = m_wallet->get_multisig_status().multisig_is_active && m_wallet->has_multisig_partial_key_images(); std::map> balance_per_subaddress_per_account; std::map>>> unlocked_balance_per_subaddress_per_account; if (req.all_accounts) @@ -1139,7 +1139,7 @@ namespace tools fill(spent_key_images, key_image_list); } - if (m_wallet->multisig()) + if (m_wallet->get_multisig_status().multisig_is_active) { multisig_txset = epee::string_tools::buff_to_hex_nodelimer(m_wallet->save_multisig_tx(ptx_vector)); if (multisig_txset.empty()) @@ -2278,10 +2278,11 @@ namespace tools if (req.key_type.compare("mnemonic") == 0) { epee::wipeable_string seed; - bool ready; - if (m_wallet->multisig(&ready)) + const multisig::multisig_account_status ms_status{m_wallet->get_multisig_status()}; + + if (ms_status.multisig_is_active) { - if (!ready) + if (!ms_status.is_ready) { er.code = WALLET_RPC_ERROR_CODE_NOT_MULTISIG; er.message = "This wallet is multisig, but not yet finalized"; @@ -4285,7 +4286,14 @@ namespace tools bool wallet_rpc_server::on_is_multisig(const wallet_rpc::COMMAND_RPC_IS_MULTISIG::request& req, wallet_rpc::COMMAND_RPC_IS_MULTISIG::response& res, epee::json_rpc::error& er, const connection_context *ctx) { if (!m_wallet) return not_open(er); - res.multisig = m_wallet->multisig(&res.ready, &res.threshold, &res.total); + const multisig::multisig_account_status ms_status{m_wallet->get_multisig_status()}; + + res.multisig = ms_status.multisig_is_active; + res.kex_is_done = ms_status.kex_is_done; + res.ready = ms_status.is_ready; + res.threshold = ms_status.threshold; + res.total = ms_status.total; + return true; } //------------------------------------------------------------------------------------------------------------------------------ @@ -4298,7 +4306,7 @@ namespace tools er.message = "Command unavailable in restricted mode."; return false; } - if (m_wallet->multisig()) + if (m_wallet->get_multisig_status().multisig_is_active) { er.code = WALLET_RPC_ERROR_CODE_ALREADY_MULTISIG; er.message = "This wallet is already multisig"; @@ -4328,7 +4336,7 @@ namespace tools er.message = "Command unavailable in restricted mode."; return false; } - if (m_wallet->multisig()) + if (m_wallet->get_multisig_status().multisig_is_active) { er.code = WALLET_RPC_ERROR_CODE_ALREADY_MULTISIG; er.message = "This wallet is already multisig"; @@ -4367,14 +4375,15 @@ namespace tools er.message = "Command unavailable in restricted mode."; return false; } - bool ready; - if (!m_wallet->multisig(&ready)) + const multisig::multisig_account_status ms_status{m_wallet->get_multisig_status()}; + + if (!ms_status.multisig_is_active) { er.code = WALLET_RPC_ERROR_CODE_NOT_MULTISIG; er.message = "This wallet is not multisig"; return false; } - if (!ready) + if (!ms_status.is_ready) { er.code = WALLET_RPC_ERROR_CODE_NOT_MULTISIG; er.message = "This wallet is multisig, but not yet finalized"; @@ -4408,15 +4417,15 @@ namespace tools er.message = "Command unavailable in restricted mode."; return false; } - bool ready; - uint32_t threshold, total; - if (!m_wallet->multisig(&ready, &threshold, &total)) + const multisig::multisig_account_status ms_status{m_wallet->get_multisig_status()}; + + if (!ms_status.multisig_is_active) { er.code = WALLET_RPC_ERROR_CODE_NOT_MULTISIG; er.message = "This wallet is not multisig"; return false; } - if (!ready) + if (!ms_status.is_ready) { er.code = WALLET_RPC_ERROR_CODE_NOT_MULTISIG; er.message = "This wallet is multisig, but not yet finalized"; @@ -4424,7 +4433,7 @@ namespace tools } CHECK_MULTISIG_ENABLED(); - if (req.info.size() < threshold - 1) + if (req.info.size() + 1 < ms_status.threshold) { er.code = WALLET_RPC_ERROR_CODE_THRESHOLD_NOT_REACHED; er.message = "Needs multisig export info from more participants"; @@ -4488,9 +4497,9 @@ namespace tools er.message = "Command unavailable in restricted mode."; return false; } - bool ready; - uint32_t threshold, total; - if (!m_wallet->multisig(&ready, &threshold, &total)) + multisig::multisig_account_status ms_status{m_wallet->get_multisig_status()}; + + if (!ms_status.multisig_is_active) { er.code = WALLET_RPC_ERROR_CODE_NOT_MULTISIG; er.message = "This wallet is not multisig"; @@ -4498,7 +4507,7 @@ namespace tools } CHECK_MULTISIG_ENABLED(); - if (req.multisig_info.size() + 1 < total) + if (req.multisig_info.size() + 1 < ms_status.total) { er.code = WALLET_RPC_ERROR_CODE_THRESHOLD_NOT_REACHED; er.message = "Needs multisig info from more participants"; @@ -4508,8 +4517,8 @@ namespace tools try { res.multisig_info = m_wallet->exchange_multisig_keys(req.password, req.multisig_info, req.force_update_use_with_caution); - m_wallet->multisig(&ready); - if (ready) + ms_status = m_wallet->get_multisig_status(); + if (ms_status.is_ready) { res.address = m_wallet->get_account().get_public_address_str(m_wallet->nettype()); } @@ -4523,6 +4532,47 @@ namespace tools return true; } //------------------------------------------------------------------------------------------------------------------------------ + bool wallet_rpc_server::on_get_multisig_key_exchange_booster(const wallet_rpc::COMMAND_RPC_GET_MULTISIG_KEY_EXCHANGE_BOOSTER::request& req, wallet_rpc::COMMAND_RPC_GET_MULTISIG_KEY_EXCHANGE_BOOSTER::response& res, epee::json_rpc::error& er, const connection_context *ctx) + { + 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; + } + const multisig::multisig_account_status ms_status{m_wallet->get_multisig_status()}; + + if (ms_status.is_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() == 0) + { + 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->get_multisig_key_exchange_booster(req.password, + req.multisig_info, + req.threshold, + req.num_signers); + } + catch (const std::exception &e) + { + er.code = WALLET_RPC_ERROR_CODE_UNKNOWN_ERROR; + er.message = std::string("Error calling exchange_multisig_info_booster: ") + 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, const connection_context *ctx) { if (!m_wallet) return not_open(er); @@ -4532,15 +4582,15 @@ namespace tools er.message = "Command unavailable in restricted mode."; return false; } - bool ready; - uint32_t threshold, total; - if (!m_wallet->multisig(&ready, &threshold, &total)) + const multisig::multisig_account_status ms_status{m_wallet->get_multisig_status()}; + + if (!ms_status.multisig_is_active) { er.code = WALLET_RPC_ERROR_CODE_NOT_MULTISIG; er.message = "This wallet is not multisig"; return false; } - if (!ready) + if (!ms_status.is_ready) { er.code = WALLET_RPC_ERROR_CODE_NOT_MULTISIG; er.message = "This wallet is multisig, but not yet finalized"; @@ -4602,15 +4652,15 @@ namespace tools er.message = "Command unavailable in restricted mode."; return false; } - bool ready; - uint32_t threshold, total; - if (!m_wallet->multisig(&ready, &threshold, &total)) + const multisig::multisig_account_status ms_status{m_wallet->get_multisig_status()}; + + if (!ms_status.multisig_is_active) { er.code = WALLET_RPC_ERROR_CODE_NOT_MULTISIG; er.message = "This wallet is not multisig"; return false; } - if (!ready) + if (!ms_status.is_ready) { er.code = WALLET_RPC_ERROR_CODE_NOT_MULTISIG; er.message = "This wallet is multisig, but not yet finalized"; @@ -4635,7 +4685,7 @@ namespace tools return false; } - if (txs.m_signers.size() < threshold) + if (txs.m_signers.size() < ms_status.threshold) { er.code = WALLET_RPC_ERROR_CODE_THRESHOLD_NOT_REACHED; er.message = "Not enough signers signed this transaction."; diff --git a/src/wallet/wallet_rpc_server.h b/src/wallet/wallet_rpc_server.h index 24f368804..b35f6853a 100644 --- a/src/wallet/wallet_rpc_server.h +++ b/src/wallet/wallet_rpc_server.h @@ -153,6 +153,7 @@ namespace tools 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("get_multisig_key_exchange_booster", on_get_multisig_key_exchange_booster, wallet_rpc::COMMAND_RPC_GET_MULTISIG_KEY_EXCHANGE_BOOSTER) 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("validate_address", on_validate_address, wallet_rpc::COMMAND_RPC_VALIDATE_ADDRESS) @@ -249,6 +250,7 @@ namespace tools 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, const connection_context *ctx = NULL); 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, const connection_context *ctx = NULL); 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, const connection_context *ctx = NULL); + bool on_get_multisig_key_exchange_booster(const wallet_rpc::COMMAND_RPC_GET_MULTISIG_KEY_EXCHANGE_BOOSTER::request& req, wallet_rpc::COMMAND_RPC_GET_MULTISIG_KEY_EXCHANGE_BOOSTER::response& res, epee::json_rpc::error& er, const connection_context *ctx = NULL); 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, const connection_context *ctx = NULL); 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, const connection_context *ctx = NULL); bool on_validate_address(const wallet_rpc::COMMAND_RPC_VALIDATE_ADDRESS::request& req, wallet_rpc::COMMAND_RPC_VALIDATE_ADDRESS::response& res, epee::json_rpc::error& er, const connection_context *ctx = NULL); diff --git a/src/wallet/wallet_rpc_server_commands_defs.h b/src/wallet/wallet_rpc_server_commands_defs.h index a51ba8680..d734f23fc 100644 --- a/src/wallet/wallet_rpc_server_commands_defs.h +++ b/src/wallet/wallet_rpc_server_commands_defs.h @@ -2392,12 +2392,14 @@ namespace wallet_rpc struct response_t { bool multisig; + bool kex_is_done; bool ready; uint32_t threshold; uint32_t total; BEGIN_KV_SERIALIZE_MAP() KV_SERIALIZE(multisig) + KV_SERIALIZE(kex_is_done) KV_SERIALIZE(ready) KV_SERIALIZE(threshold) KV_SERIALIZE(total) @@ -2548,6 +2550,35 @@ namespace wallet_rpc typedef epee::misc_utils::struct_init response; }; + struct COMMAND_RPC_GET_MULTISIG_KEY_EXCHANGE_BOOSTER + { + struct request_t + { + std::string password; + std::vector multisig_info; + uint32_t threshold; + uint32_t num_signers; + + BEGIN_KV_SERIALIZE_MAP() + KV_SERIALIZE(password) + KV_SERIALIZE(multisig_info) + KV_SERIALIZE(threshold) + KV_SERIALIZE(num_signers) + END_KV_SERIALIZE_MAP() + }; + typedef epee::misc_utils::struct_init request; + + struct response_t + { + std::string multisig_info; + + BEGIN_KV_SERIALIZE_MAP() + KV_SERIALIZE(multisig_info) + END_KV_SERIALIZE_MAP() + }; + typedef epee::misc_utils::struct_init response; + }; + struct COMMAND_RPC_SIGN_MULTISIG { struct request_t