Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

multisig: add key exchange round booster #8203

Merged
merged 1 commit into from
Jul 16, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
45 changes: 35 additions & 10 deletions src/multisig/multisig_account.h
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down Expand Up @@ -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<multisig_kex_msg> &expanded_msgs,
const bool force_update_use_with_caution = false);
void kex_update(const std::vector<multisig_kex_msg> &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<multisig_kex_msg> &expanded_msgs) const;

private:
// implementation of kex_update() (non-transactional)
void kex_update_impl(const std::vector<multisig_kex_msg> &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<crypto::public_key> 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<multisig_kex_msg> &expanded_msgs,
const std::uint32_t kex_rounds_required,
std::vector<crypto::public_key> &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)
Expand Down
173 changes: 121 additions & 52 deletions src/multisig/multisig_account_kex_impl.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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.
Expand All @@ -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
*/
//----------------------------------------------------------------------------------------------------------------------
Expand Down Expand Up @@ -684,59 +684,67 @@ namespace multisig
//----------------------------------------------------------------------------------------------------------------------
// multisig_account: INTERNAL
//----------------------------------------------------------------------------------------------------------------------
void multisig_account::initialize_kex_update(const std::vector<multisig_kex_msg> &expanded_msgs,
const std::uint32_t kex_rounds_required,
std::vector<crypto::public_key> &exclude_pubkeys_out)
std::vector<crypto::public_key> multisig_account::get_kex_exclude_pubkeys() const
{
// exclude all keys the local account recommends
std::vector<crypto::public_key> exclude_pubkeys;

if (m_kex_rounds_complete == 0)
{
// the first round of kex msgs will contain each participant's base pubkeys and ancillary privkeys
// 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, 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.emplace_back(shared_key_with_origins.first);
}

// 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<crypto::secret_key> participant_base_common_privkeys;
participant_base_common_privkeys.reserve(expanded_msgs.size() + 1);
return exclude_pubkeys;
}
//----------------------------------------------------------------------------------------------------------------------
// multisig_account: INTERNAL
//----------------------------------------------------------------------------------------------------------------------
void multisig_account::initialize_kex_update(const std::vector<multisig_kex_msg> &expanded_msgs,
const std::uint32_t kex_rounds_required)
{
// initialization is only needed during the first round
if (m_kex_rounds_complete > 0)
return;

// add local ancillary base privkey
participant_base_common_privkeys.emplace_back(m_base_common_privkey);
// the first round of kex msgs will contain each participant's base pubkeys and ancillary privkeys, so we prepare
// them here

// 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());
}
}
// 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<crypto::secret_key> participant_base_common_privkeys;
participant_base_common_privkeys.reserve(expanded_msgs.size() + 1);

// make common privkey
make_multisig_common_privkey(std::move(participant_base_common_privkeys), m_common_privkey);
// add local ancillary base privkey
participant_base_common_privkeys.emplace_back(m_base_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");
// 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());
}

// 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);
}
// make common privkey
make_multisig_common_privkey(std::move(participant_base_common_privkeys), m_common_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);
}
else
{
// in other rounds, kex msgs will contain participants' shared keys
// 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");

// 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);
}
// 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);
}
}
//----------------------------------------------------------------------------------------------------------------------
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -863,8 +870,7 @@ namespace multisig
"Multisig kex has already completed all required rounds (including post-kex verification).");

// initialize account update
std::vector<crypto::public_key> 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;
Expand All @@ -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<multisig_kex_msg> &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<crypto::public_key> 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
15 changes: 15 additions & 0 deletions src/wallet/api/wallet.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -1354,6 +1354,21 @@ std::string WalletImpl::exchangeMultisigKeys(const std::vector<std::string> &inf
return string();
}

std::string WalletImpl::getMultisigKeyExchangeBooster(const std::vector<std::string> &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();
Expand Down
1 change: 1 addition & 0 deletions src/wallet/api/wallet.h
Original file line number Diff line number Diff line change
Expand Up @@ -148,6 +148,7 @@ class WalletImpl : public Wallet
std::string getMultisigInfo() const override;
std::string makeMultisig(const std::vector<std::string>& info, uint32_t threshold) override;
std::string exchangeMultisigKeys(const std::vector<std::string> &info, const bool force_update_use_with_caution = false) override;
std::string getMultisigKeyExchangeBooster(const std::vector<std::string> &info, const uint32_t threshold, const uint32_t num_signers) override;
bool exportMultisigImages(std::string& images) override;
size_t importMultisigImages(const std::vector<std::string>& images) override;
bool hasMultisigPartialKeyImages() const override;
Expand Down
9 changes: 9 additions & 0 deletions src/wallet/api/wallet2_api.h
Original file line number Diff line number Diff line change
Expand Up @@ -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<std::string> &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<std::string> &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
Expand Down
Loading
Loading