wallet2: refactor multisig ignore sets logic into separate functions
What changed, and why it matters
This commit is a code cleanup: it moves existing multisig signer-selection logic out of the middle of a large transaction-building function into two smaller, named helper functions. The actual behavior appears unchanged. There is no indication this fixes a security bug or introduces a vulnerability.
No security action required. Treat as ordinary refactoring; review for code quality if desired.
Security signals we found
No strong security signals were identified.
Evidence from the diff
The patch refactors wallet2’s multisig ignore-set logic. It extracts inline code from transfer_selected_rct() into two new private methods: multisig_available_signers() and multisig_attempt_ignore_sets(). The logic for determining available signers, checking the threshold, building combinations of ignored signers, and defaulting to a single empty ignore set is preserved. The only functional change is that the empty ignore set is now added inside the helper when n_available_signers equals the threshold, rather than later in the caller. No algorithmic or trust-boundary changes are visible.
Changed components
src/wallet/wallet2.cppsrc/wallet/wallet2.hInspect captured patch +84 / −61
diff --git a/src/wallet/wallet2.cpp b/src/wallet/wallet2.cpp
index 4336075..5479aae 100644
--- a/src/wallet/wallet2.cpp
+++ b/src/wallet/wallet2.cpp
@@ -9879,65 +9879,9 @@ void wallet2::transfer_selected_rct(std::vector<cryptonote::tx_destination_entry
THROW_WALLET_EXCEPTION_IF(needed_money < dt.amount, error::tx_sum_overflow, dsts, fee, m_nettype);
}
- // if this is a multisig wallet, create a list of multisig signers we can use
- std::deque<crypto::public_key> multisig_signers;
- size_t n_multisig_txes = 0;
- std::vector<std::unordered_set<crypto::public_key>> ignore_sets;
- if (m_multisig && !m_transfers.empty())
- {
- const crypto::public_key local_signer = get_multisig_signer_public_key();
- size_t n_available_signers = 1;
-
- // At this step we need to define set of participants available for signature,
- // i.e. those of them who exchanged with multisig info's
- // note: The oldest unspent owned output's multisig info (in m_transfers) will contain the most recent result of
- // 'import_multisig()', which means only 'fresh' multisig infos (public nonces) will be used to make tx attempts.
- // - If a signer's info was missing from the latest call to 'import_multisig()', then they won't be able to participate!
- // - If a newly-acquired output doesn't have enough nonces from multisig infos, then it can't be spent!
- for (const crypto::public_key &signer: m_multisig_signers)
- {
- if (signer == local_signer)
- continue;
- for (const auto &i: m_transfers[0].m_multisig_info)
- {
- if (i.m_signer == signer)
- {
- multisig_signers.push_back(signer);
- ++n_available_signers;
- break;
- }
- }
- }
- // n_available_signers includes the transaction creator, but multisig_signers doesn't
- MDEBUG("We can use " << n_available_signers << "/" << m_multisig_signers.size() << " other signers");
- THROW_WALLET_EXCEPTION_IF(n_available_signers < m_multisig_threshold, error::multisig_import_needed);
- if (n_available_signers > m_multisig_threshold)
- {
- // If there more potential signers (those who exchanged with multisig info)
- // than threshold needed some of them should be skipped since we don't know
- // who will sign tx and who won't. Hence we don't contribute their LR pairs to the signature.
-
- // We create as many transactions as many combinations of excluded signers may be.
- // For example, if we have 2/4 wallet and wallets are: A, B, C and D. Let A be
- // transaction creator, so we need just 1 signature from set of B, C, D.
- // Using "excluding" logic here we have to exclude 2-of-3 wallets. Combinations go as follows:
- // BC, BD, and CD. We save these sets to use later and counting the number of required txs.
- tools::Combinator<crypto::public_key> c(std::vector<crypto::public_key>(multisig_signers.begin(), multisig_signers.end()));
- auto ignore_combinations = c.combine(multisig_signers.size() + 1 - m_multisig_threshold);
- for (const auto& combination: ignore_combinations)
- {
- ignore_sets.push_back(std::unordered_set<crypto::public_key>(combination.begin(), combination.end()));
- }
-
- n_multisig_txes = ignore_sets.size();
- }
- else
- {
- // If we have exact count of signers just to fit in threshold we don't exclude anyone and create 1 transaction
- n_multisig_txes = 1;
- }
- MDEBUG("We will create " << n_multisig_txes << " txes");
- }
+ // if this is a multisig wallet, get a list of signing attempts to make; each attempt contains a set of signers
+ // to ignore for the attempt (all other signers may participate)
+ std::vector<std::unordered_set<crypto::public_key>> ignore_sets{this->multisig_attempt_ignore_sets()};
bool all_rct = true;
uint64_t found_money = 0;
@@ -10094,8 +10038,6 @@ void wallet2::transfer_selected_rct(std::vector<cryptonote::tx_destination_entry
std::vector<tools::wallet2::multisig_sig> multisig_sigs;
if (m_multisig) {
- if (ignore_sets.empty())
- ignore_sets.emplace_back();
const std::size_t num_multisig_attempts = ignore_sets.size();
multisig_sigs.resize(num_multisig_attempts);
std::unordered_set<rct::key> all_used_L;
@@ -14516,6 +14458,85 @@ crypto::key_image wallet2::get_multisig_composite_key_image(size_t n) const
return ki;
}
//----------------------------------------------------------------------------------------------------
+// Gets the set of participants available for a signature, i.e. those who exchanged multisig infos with us.
+//
+// Assumes the oldest unspent owned output's multisig info (in m_transfers) will contain the most recent result of
+// 'import_multisig()', which means only 'fresh' multisig infos (public nonces) will be used to make tx attempts.
+// - If a signer's info was missing from the latest call to 'import_multisig()', then they won't be able to participate!
+// - If a newly-acquired output doesn't have enough nonces from multisig infos, then it can't be spent!
+std::deque<crypto::public_key> wallet2::multisig_available_signers() const
+{
+ std::deque<crypto::public_key> available_signers{};
+ if (!m_multisig || m_transfers.empty()) { return {}; }
+
+ const crypto::public_key local_signer = this->get_multisig_signer_public_key();
+
+ for (const crypto::public_key &signer : m_multisig_signers)
+ {
+ if (signer == local_signer)
+ continue;
+ for (const auto &i: m_transfers[0].m_multisig_info)
+ {
+ if (i.m_signer == signer)
+ {
+ available_signers.push_back(signer);
+ break;
+ }
+ }
+ }
+
+ return available_signers;
+}
+//----------------------------------------------------------------------------------------------------
+// Sets of multisig signers who should be ignored from a multisig tx signing attempt.
+// One set exists for each combination of available signers equal to the multisig threshold.
+// An empty set is returned if there is only one combination available.
+std::vector<std::unordered_set<crypto::public_key>> wallet2::multisig_attempt_ignore_sets() const
+{
+ std::vector<std::unordered_set<crypto::public_key>> ignore_sets{};
+
+ if (!m_multisig) { return {}; }
+
+ // Get signers who provided nonces recently.
+ std::deque<crypto::public_key> available_signers{this->multisig_available_signers()};
+
+ // +1 for the local signer
+ size_t n_available_signers = 1 + available_signers.size();
+ MDEBUG("We can use " << n_available_signers << "/" << m_multisig_signers.size() << " other signers");
+ THROW_WALLET_EXCEPTION_IF(n_available_signers < m_multisig_threshold, error::multisig_import_needed);
+
+ size_t n_multisig_txes = 0;
+ if (n_available_signers > m_multisig_threshold)
+ {
+ // If there more potential signers (those who exchanged with multisig info)
+ // than m_multisig_threshold needed, some of them should be skipped since we don't know
+ // who will sign the tx and who won't. Hence we don't contribute their LR pairs to the signature.
+
+ // We create as many transactions as many combinations of excluded signers may be.
+ // For example, if we have 2/4 wallet and wallets are: A, B, C and D. Let A be
+ // transaction creator, so we need just 1 signature from set of B, C, D.
+ // Using "excluding" logic here we have to exclude 2-of-3 wallets. Combinations go as follows:
+ // BC, BD, and CD. We save these sets to use later and counting the number of required txs.
+ tools::Combinator<crypto::public_key> c(std::vector<crypto::public_key>(available_signers.begin(), available_signers.end()));
+ auto ignore_combinations = c.combine(available_signers.size() + 1 - m_multisig_threshold);
+ for (const auto& combination: ignore_combinations)
+ {
+ ignore_sets.push_back(std::unordered_set<crypto::public_key>(combination.begin(), combination.end()));
+ }
+
+ n_multisig_txes = ignore_sets.size();
+ }
+ else //(n_available_signers == m_multisig_threshold)
+ {
+ // If we have exact count of signers just to fit in m_multisig_threshold we don't exclude anyone and create 1 transaction
+ n_multisig_txes = 1;
+ ignore_sets.emplace_back();
+ }
+
+ MDEBUG("We will create " << n_multisig_txes << " txes");
+ return ignore_sets;
+}
+//----------------------------------------------------------------------------------------------------
cryptonote::blobdata wallet2::export_multisig()
{
std::vector<tools::wallet2::multisig_info> info;
diff --git a/src/wallet/wallet2.h b/src/wallet/wallet2.h
index 145f733..6613d32 100644
--- a/src/wallet/wallet2.h
+++ b/src/wallet/wallet2.h
@@ -1868,6 +1868,8 @@ private:
rct::multisig_kLRki get_multisig_composite_kLRki(size_t n, const std::unordered_set<crypto::public_key> &ignore_set, std::unordered_set<rct::key> &used_L, std::unordered_set<rct::key> &new_used_L) const;
rct::multisig_kLRki get_multisig_kLRki(size_t n, const rct::key &k) const;
void get_multisig_k(size_t idx, const std::unordered_set<rct::key> &used_L, rct::key &nonce);
+ std::deque<crypto::public_key> multisig_available_signers() const;
+ std::vector<std::unordered_set<crypto::public_key>> multisig_attempt_ignore_sets() const;
void update_multisig_rescan_info(const std::vector<std::vector<rct::key>> &multisig_k, const std::vector<std::vector<tools::wallet2::multisig_info>> &info, size_t n);
bool add_rings(const crypto::chacha_key &key, const cryptonote::transaction_prefix &tx);
bool add_rings(const cryptonote::transaction_prefix &tx);
Why this scored 12/100
Community notes
Notes can correct, qualify, or add evidence to the AI analysis. Every note shown here has been validated by a human moderator.
The AI analysis stands alone for now. Submit a note if you can add evidence or important context.