diff --git a/src/multisig/multisig_account.h b/src/multisig/multisig_account.h index 2ea8d0133..dc2fbabd8 100644 --- a/src/multisig/multisig_account.h +++ b/src/multisig/multisig_account.h @@ -103,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); @@ -190,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 ef0acf307..9be760e78 100644 --- a/src/multisig/multisig_account_kex_impl.cpp +++ b/src/multisig/multisig_account_kex_impl.cpp @@ -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 @@ -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/wallet/api/wallet.cpp b/src/wallet/api/wallet.cpp index 58cb84947..a6dabddb6 100644 --- a/src/wallet/api/wallet.cpp +++ b/src/wallet/api/wallet.cpp @@ -1354,6 +1354,21 @@ std::string WalletImpl::exchangeMultisigKeys(const std::vector &inf return string(); } +std::string WalletImpl::getMultisigKeyExchangeBooster(const std::vector &info, + const std::uint32_t threshold, + const std::uint32_t num_signers) { + try { + clearStatus(); + + return m_wallet->get_multisig_key_exchange_booster(epee::wipeable_string(m_password), info, threshold, num_signers); + } catch (const exception& e) { + LOG_ERROR("Error on boosting multisig key exchange: " << e.what()); + setStatusError(string(tr("Failed to boost multisig key exchange: ")) + e.what()); + } + + return string(); +} + bool WalletImpl::exportMultisigImages(string& images) { try { clearStatus(); diff --git a/src/wallet/api/wallet.h b/src/wallet/api/wallet.h index d1bf4f759..7ca126dd7 100644 --- a/src/wallet/api/wallet.h +++ b/src/wallet/api/wallet.h @@ -148,6 +148,7 @@ public: std::string getMultisigInfo() const override; std::string makeMultisig(const std::vector& info, uint32_t threshold) override; std::string exchangeMultisigKeys(const std::vector &info, const bool force_update_use_with_caution = false) override; + std::string getMultisigKeyExchangeBooster(const std::vector &info, const uint32_t threshold, const uint32_t num_signers) override; bool exportMultisigImages(std::string& images) override; size_t importMultisigImages(const std::vector& images) override; bool hasMultisigPartialKeyImages() const override; diff --git a/src/wallet/api/wallet2_api.h b/src/wallet/api/wallet2_api.h index 53210832b..bd641fc0e 100644 --- a/src/wallet/api/wallet2_api.h +++ b/src/wallet/api/wallet2_api.h @@ -802,6 +802,15 @@ struct Wallet * @return new info string if more rounds required or an empty string if wallet creation is done */ virtual std::string exchangeMultisigKeys(const std::vector &info, const bool force_update_use_with_caution) = 0; + /** + * @brief getMultisigKeyExchangeBooster - obtain partial information for the key exchange round after the in-progress round, + * to speed up another signer's key exchange process + * @param info - base58 encoded key derivations returned by makeMultisig or exchangeMultisigKeys function call + * @param threshold - number of required signers to make valid transaction. Must be <= number of participants. + * @param num_signers - total number of multisig participants. + * @return new info string if more rounds required or exception if no more rounds (i.e. no rounds to boost) + */ + virtual std::string getMultisigKeyExchangeBooster(const std::vector &info, const uint32_t threshold, const uint32_t num_signers) = 0; /** * @brief exportMultisigImages - exports transfers' key images * @param images - output paramter for hex encoded array of images diff --git a/src/wallet/wallet2.cpp b/src/wallet/wallet2.cpp index 9b09d0920..951e8cb6a 100644 --- a/src/wallet/wallet2.cpp +++ b/src/wallet/wallet2.cpp @@ -5509,32 +5509,77 @@ void wallet2::restore(const std::string& wallet_, const epee::wipeable_string& p } } //---------------------------------------------------------------------------------------------------- -std::string wallet2::make_multisig(const epee::wipeable_string &password, - const std::vector &initial_kex_msgs, - const std::uint32_t threshold) +epee::misc_utils::auto_scope_leave_caller wallet2::decrypt_account_for_multisig(const epee::wipeable_string &password) { // decrypt account keys + // note: this conditional's clauses are old and undocumented 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); + this->decrypt_keys(chacha_key); keys_reencryptor = epee::misc_utils::create_scope_leave_handler( - [&, this, chacha_key]() + [this, chacha_key]() { - m_account.encrypt_keys(chacha_key); - m_account.decrypt_viewkey(chacha_key); + this->encrypt_keys(chacha_key); } ); } - // create multisig account - multisig::multisig_account multisig_account{ - multisig::get_multisig_blinded_secret_key(get_account().get_keys().m_spend_secret_key), - multisig::get_multisig_blinded_secret_key(get_account().get_keys().m_view_secret_key) + return keys_reencryptor; +} +//---------------------------------------------------------------------------------------------------- +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{this->decrypt_account_for_multisig(password)}; + + // create multisig account + multisig::multisig_account multisig_account; + this->get_uninitialized_multisig_account(multisig_account); // open initial kex messages, validate them, extract signers std::vector expanded_msgs; @@ -5542,7 +5587,7 @@ std::string wallet2::make_multisig(const epee::wipeable_string &password, expanded_msgs.reserve(initial_kex_msgs.size()); signers.reserve(initial_kex_msgs.size() + 1); - for (const auto &msg : initial_kex_msgs) + for (const std::string &msg : initial_kex_msgs) { expanded_msgs.emplace_back(msg); @@ -5551,17 +5596,19 @@ std::string wallet2::make_multisig(const epee::wipeable_string &password, CHECK_AND_ASSERT_THROW_MES(expanded_msgs.back().get_round() == 1, "Trying to make multisig with message that has invalid multisig kex round (should be '1')."); - // 2. duplicate signers not allowed + // 2. duplicate signers not allowed (the number of signers is implied by the number of initial kex messages passed + // in, so we can't just ignore duplicates here) CHECK_AND_ASSERT_THROW_MES(std::find(signers.begin(), signers.end(), expanded_msgs.back().get_signing_pubkey()) == signers.end(), "Duplicate signers not allowed when converting a wallet to multisig."); - // add signer (skip self for now) - if (expanded_msgs.back().get_signing_pubkey() != multisig_account.get_base_pubkey()) - signers.push_back(expanded_msgs.back().get_signing_pubkey()); + // add signer + signers.push_back(expanded_msgs.back().get_signing_pubkey()); } - // add self to signers - signers.push_back(multisig_account.get_base_pubkey()); + // expect that self is in the input list (this guarantees that the input list size always equals the number of intended + // signers for the account [when combined with duplicate checking]) + CHECK_AND_ASSERT_THROW_MES(std::find(signers.begin(), signers.end(), multisig_account.get_base_pubkey()) != signers.end(), + "The local account's signer key was not found in initial multisig kex messages when converting a wallet to multisig."); // intialize key exchange multisig_account.initialize_kex(threshold, signers, expanded_msgs); @@ -5572,12 +5619,13 @@ std::string wallet2::make_multisig(const epee::wipeable_string &password, { // Save the original i.e. non-multisig keys so the MMS can continue to use them to encrypt and decrypt messages // (making a wallet multisig overwrites those keys, see account_base::make_multisig) - m_original_address = get_account().get_keys().m_account_address; - m_original_view_secret_key = get_account().get_keys().m_view_secret_key; + m_original_address = this->get_account().get_keys().m_account_address; + m_original_view_secret_key = this->get_account().get_keys().m_view_secret_key; m_original_keys_available = true; } - clear(); + // clear wallet caches + this->clear(); // account base MINFO("Creating multisig address..."); @@ -5587,14 +5635,14 @@ std::string wallet2::make_multisig(const epee::wipeable_string &password, multisig_account.get_multisig_privkeys()), "Failed to create multisig wallet account due to bad keys"); - init_type(hw::device::device_type::SOFTWARE); + this->init_type(hw::device::device_type::SOFTWARE); m_original_keys_available = true; m_multisig = true; m_multisig_threshold = threshold; m_multisig_signers = signers; m_multisig_rounds_passed = 1; - // derivations stored (should be empty in last round) + // derivations stored (note: should be empty in last kex round) m_multisig_derivations.clear(); m_multisig_derivations.reserve(multisig_account.get_kex_keys_to_origins_map().size()); @@ -5608,12 +5656,12 @@ std::string wallet2::make_multisig(const epee::wipeable_string &password, keys_reencryptor = epee::misc_utils::auto_scope_leave_caller(); 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")); - setup_new_blockchain(); + this->setup_new_blockchain(); if (!m_wallet_file.empty()) - store(); + this->store(); return multisig_account.get_next_kex_round_msg(); } @@ -5622,45 +5670,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*/) { - const multisig::multisig_account_status ms_status{get_multisig_status()}; + 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); - } - ); - } + epee::misc_utils::auto_scope_leave_caller keys_reencryptor{this->decrypt_account_for_multisig(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) @@ -5676,7 +5694,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 @@ -5712,27 +5730,27 @@ std::string wallet2::exchange_multisig_keys(const epee::wipeable_string &passwor 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(); } @@ -5740,16 +5758,63 @@ std::string wallet2::exchange_multisig_keys(const epee::wipeable_string &passwor std::string wallet2::get_multisig_first_kex_msg() const { // create multisig account - multisig::multisig_account multisig_account{ - // k_base = H(normal private spend key) - multisig::get_multisig_blinded_secret_key(get_account().get_keys().m_spend_secret_key), - // k_view = H(normal private view key) - multisig::get_multisig_blinded_secret_key(get_account().get_keys().m_view_secret_key) - }; + multisig::multisig_account multisig_account; + this->get_uninitialized_multisig_account(multisig_account); return multisig_account.get_next_kex_round_msg(); } //---------------------------------------------------------------------------------------------------- +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) +{ + CHECK_AND_ASSERT_THROW_MES(kex_messages.size() > 0, "No key exchange messages passed in."); + + // decrypt account keys + epee::misc_utils::auto_scope_leave_caller keys_reencryptor{this->decrypt_account_for_multisig(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) + { + // 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; diff --git a/src/wallet/wallet2.h b/src/wallet/wallet2.h index 5f2f0e0c4..d06dc678b 100644 --- a/src/wallet/wallet2.h +++ b/src/wallet/wallet2.h @@ -86,6 +86,7 @@ class Serialization_portability_wallet_Test; class wallet_accessor_test; +namespace multisig { class multisig_account; } namespace tools { @@ -892,6 +893,23 @@ private: */ void restore(const std::string& wallet_, const epee::wipeable_string& password, const std::string &device_name, bool create_address_file = false); + private: + /*! + * \brief Decrypts the account keys + * \return an RAII reencryptor for the account keys + */ + epee::misc_utils::auto_scope_leave_caller decrypt_account_for_multisig(const epee::wipeable_string &password); + /*! + * \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 @@ -913,6 +931,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 diff --git a/src/wallet/wallet_rpc_server.cpp b/src/wallet/wallet_rpc_server.cpp index 376c58f89..86bfadeae 100644 --- a/src/wallet/wallet_rpc_server.cpp +++ b/src/wallet/wallet_rpc_server.cpp @@ -4248,6 +4248,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); diff --git a/src/wallet/wallet_rpc_server.h b/src/wallet/wallet_rpc_server.h index bfb7013e2..bc025d138 100644 --- a/src/wallet/wallet_rpc_server.h +++ b/src/wallet/wallet_rpc_server.h @@ -151,6 +151,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) @@ -242,6 +243,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 2173f5b6e..acf24c87b 100644 --- a/src/wallet/wallet_rpc_server_commands_defs.h +++ b/src/wallet/wallet_rpc_server_commands_defs.h @@ -2481,6 +2481,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 diff --git a/tests/unit_tests/multisig.cpp b/tests/unit_tests/multisig.cpp index c044591c1..1b3a455c0 100644 --- a/tests/unit_tests/multisig.cpp +++ b/tests/unit_tests/multisig.cpp @@ -149,6 +149,7 @@ static void check_results(const std::vector &intermediate_infos, std::unordered_set unique_privkeys; rct::key composite_pubkey = rct::identity(); + ASSERT_TRUE(wallets.size() > 0); wallets[0].decrypt_keys(""); crypto::public_key spend_pubkey = wallets[0].get_account().get_keys().m_account_address.m_spend_public_key; crypto::secret_key view_privkey = wallets[0].get_account().get_keys().m_view_secret_key; @@ -156,32 +157,48 @@ static void check_results(const std::vector &intermediate_infos, EXPECT_TRUE(crypto::secret_key_to_public_key(view_privkey, view_pubkey)); wallets[0].encrypt_keys(""); - for (size_t i = 0; i < wallets.size(); ++i) + // at the end of multisig kex, all wallets should emit a post-kex message with the same two pubkeys + std::vector post_kex_msg_pubkeys; + ASSERT_TRUE(intermediate_infos.size() == wallets.size()); + for (const std::string &intermediate_info : intermediate_infos) { - EXPECT_TRUE(!intermediate_infos[i].empty()); - const multisig::multisig_account_status ms_status{wallets[i].get_multisig_status()}; + multisig::multisig_kex_msg post_kex_msg; + EXPECT_TRUE(!intermediate_info.empty()); + EXPECT_NO_THROW(post_kex_msg = intermediate_info); + + if (post_kex_msg_pubkeys.size() != 0) + EXPECT_TRUE(post_kex_msg_pubkeys == post_kex_msg.get_msg_pubkeys()); //assumes sorting is always the same + else + post_kex_msg_pubkeys = post_kex_msg.get_msg_pubkeys(); + + EXPECT_TRUE(post_kex_msg_pubkeys.size() == 2); + } + + // the post-kex pubkeys should equal the account's public view and spend keys + EXPECT_TRUE(std::find(post_kex_msg_pubkeys.begin(), post_kex_msg_pubkeys.end(), spend_pubkey) != post_kex_msg_pubkeys.end()); + EXPECT_TRUE(std::find(post_kex_msg_pubkeys.begin(), post_kex_msg_pubkeys.end(), view_pubkey) != post_kex_msg_pubkeys.end()); + + // each wallet should have the same state (private view key, public spend key), and the public spend key should be + // reproducible from the private spend keys found in each account + for (tools::wallet2 &wallet : wallets) + { + wallet.decrypt_keys(""); + const multisig::multisig_account_status ms_status{wallet.get_multisig_status()}; EXPECT_TRUE(ms_status.multisig_is_active); EXPECT_TRUE(ms_status.kex_is_done); EXPECT_TRUE(ms_status.is_ready); EXPECT_TRUE(ms_status.threshold == M); EXPECT_TRUE(ms_status.total == wallets.size()); - wallets[i].decrypt_keys(""); - - if (i != 0) - { - // "equals" is transitive relation so we need only to compare first wallet's address to each others' addresses. - // no need to compare 0's address with itself. - EXPECT_TRUE(wallets[0].get_account().get_public_address_str(cryptonote::TESTNET) == - wallets[i].get_account().get_public_address_str(cryptonote::TESTNET)); - - EXPECT_EQ(spend_pubkey, wallets[i].get_account().get_keys().m_account_address.m_spend_public_key); - EXPECT_EQ(view_privkey, wallets[i].get_account().get_keys().m_view_secret_key); - EXPECT_EQ(view_pubkey, wallets[i].get_account().get_keys().m_account_address.m_view_public_key); - } + EXPECT_TRUE(wallets[0].get_account().get_public_address_str(cryptonote::TESTNET) == + wallet.get_account().get_public_address_str(cryptonote::TESTNET)); + + EXPECT_EQ(spend_pubkey, wallet.get_account().get_keys().m_account_address.m_spend_public_key); + EXPECT_EQ(view_privkey, wallet.get_account().get_keys().m_view_secret_key); + EXPECT_EQ(view_pubkey, wallet.get_account().get_keys().m_account_address.m_view_public_key); // sum together unique multisig keys - for (const auto &privkey : wallets[i].get_account().get_keys().m_multisig_keys) + for (const auto &privkey : wallet.get_account().get_keys().m_multisig_keys) { EXPECT_NE(privkey, crypto::null_skey); @@ -189,17 +206,17 @@ static void check_results(const std::vector &intermediate_infos, { unique_privkeys.insert(privkey); crypto::public_key pubkey; - crypto::secret_key_to_public_key(privkey, pubkey); + EXPECT_TRUE(crypto::secret_key_to_public_key(privkey, pubkey)); EXPECT_NE(privkey, crypto::null_skey); EXPECT_NE(pubkey, crypto::null_pkey); EXPECT_NE(pubkey, rct::rct2pk(rct::identity())); rct::addKeys(composite_pubkey, composite_pubkey, rct::pk2rct(pubkey)); } } - wallets[i].encrypt_keys(""); + wallet.encrypt_keys(""); } - // final key via sums should equal the wallets' public spend key + // final key via sum of privkeys should equal the wallets' public spend key wallets[0].decrypt_keys(""); EXPECT_EQ(wallets[0].get_account().get_keys().m_account_address.m_spend_public_key, rct::rct2pk(composite_pubkey)); wallets[0].encrypt_keys(""); @@ -257,6 +274,104 @@ static void make_wallets(const unsigned int M, const unsigned int N, const bool check_results(intermediate_infos, wallets, M); } +static void make_wallets_boosting(std::vector& wallets, unsigned int M) +{ + ASSERT_TRUE(wallets.size() > 1 && wallets.size() <= KEYS_COUNT); + ASSERT_TRUE(M <= wallets.size()); + std::uint32_t kex_rounds_required = multisig::multisig_kex_rounds_required(wallets.size(), M); + std::uint32_t rounds_required = multisig::multisig_setup_rounds_required(wallets.size(), M); + std::uint32_t rounds_complete{0}; + + // initialize wallets, get first round multisig kex msgs + std::vector initial_infos(wallets.size()); + + for (size_t i = 0; i < wallets.size(); ++i) + { + make_wallet(i, wallets[i]); + + wallets[i].decrypt_keys(""); + initial_infos[i] = wallets[i].get_multisig_first_kex_msg(); + wallets[i].encrypt_keys(""); + } + + // wallets should not be multisig yet + for (const auto &wallet: wallets) + { + const multisig::multisig_account_status ms_status{wallet.get_multisig_status()}; + ASSERT_FALSE(ms_status.multisig_is_active); + } + + // get round 2 booster messages for wallet0 (if appropriate) + auto initial_infos_truncated = initial_infos; + initial_infos_truncated.erase(initial_infos_truncated.begin()); + + std::vector wallet0_booster_infos; + wallet0_booster_infos.reserve(wallets.size() - 1); + + if (rounds_complete + 1 < kex_rounds_required) + { + for (size_t i = 1; i < wallets.size(); ++i) + { + wallet0_booster_infos.push_back( + wallets[i].get_multisig_key_exchange_booster("", initial_infos_truncated, M, wallets.size()) + ); + } + } + + // make wallets multisig + std::vector intermediate_infos(wallets.size()); + + for (size_t i = 0; i < wallets.size(); ++i) + intermediate_infos[i] = wallets[i].make_multisig("", initial_infos, M); + + ++rounds_complete; + + // perform all kex rounds + // boost wallet0 each round, so wallet0 is always 1 round ahead + std::string wallet0_intermediate_info; + std::vector new_infos(intermediate_infos.size()); + multisig::multisig_account_status ms_status{wallets[0].get_multisig_status()}; + while (!ms_status.is_ready) + { + // use booster infos to update wallet0 'early' + if (rounds_complete < kex_rounds_required) + new_infos[0] = wallets[0].exchange_multisig_keys("", wallet0_booster_infos); + else + { + // force update the post-kex round with wallet0's post-kex message since wallet0 is 'ahead' of the other wallets + wallet0_booster_infos = {wallets[0].exchange_multisig_keys("", {})}; + new_infos[0] = wallets[0].exchange_multisig_keys("", wallet0_booster_infos, true); + } + + // get wallet0 booster infos for next round + if (rounds_complete + 1 < kex_rounds_required) + { + // remove wallet0 info for this round (so boosters have incomplete kex message set) + auto intermediate_infos_truncated = intermediate_infos; + intermediate_infos_truncated.erase(intermediate_infos_truncated.begin()); + + // obtain booster messages from all other wallets + for (size_t i = 1; i < wallets.size(); ++i) + { + wallet0_booster_infos[i-1] = + wallets[i].get_multisig_key_exchange_booster("", intermediate_infos_truncated, M, wallets.size()); + } + } + + // update other wallets + for (size_t i = 1; i < wallets.size(); ++i) + new_infos[i] = wallets[i].exchange_multisig_keys("", intermediate_infos); + + intermediate_infos = new_infos; + ++rounds_complete; + ms_status = wallets[0].get_multisig_status(); + } + + EXPECT_EQ(rounds_required, rounds_complete); + + check_results(intermediate_infos, wallets, M); +} + TEST(multisig, make_1_2) { make_wallets(1, 2, false); @@ -293,6 +408,12 @@ TEST(multisig, make_2_4) make_wallets(2, 4, true); } +TEST(multisig, make_2_4_boosting) +{ + std::vector wallets(4); + make_wallets_boosting(wallets, 2); +} + TEST(multisig, multisig_kex_msg) { using namespace multisig; diff --git a/utils/python-rpc/framework/wallet.py b/utils/python-rpc/framework/wallet.py index 8fa3eaafd..2c8ba6e91 100644 --- a/utils/python-rpc/framework/wallet.py +++ b/utils/python-rpc/framework/wallet.py @@ -540,6 +540,20 @@ class Wallet(object): } return self.rpc.send_json_rpc_request(exchange_multisig_keys) + def get_multisig_key_exchange_booster(self, multisig_info, threshold, num_signers, password = ''): + exchange_multisig_keys = { + 'method': 'get_multisig_key_exchange_booster', + 'params' : { + 'multisig_info': multisig_info, + 'threshold': threshold, + 'num_signers': num_signers, + 'password': password, + }, + 'jsonrpc': '2.0', + 'id': '0' + } + return self.rpc.send_json_rpc_request(get_multisig_key_exchange_booster) + def export_multisig_info(self): export_multisig_info = { 'method': 'export_multisig_info',