Skip to content

Commit 03a379e

Browse files
committed
wallet: more robust reorg handling for multisig info messages
1 parent d885347 commit 03a379e

File tree

4 files changed

+162
-82
lines changed

4 files changed

+162
-82
lines changed

src/wallet/wallet2.cpp

Lines changed: 137 additions & 76 deletions
Original file line numberDiff line numberDiff line change
@@ -2194,10 +2194,9 @@ void wallet2::scan_output(const cryptonote::transaction &tx, bool miner_tx, cons
21942194
THROW_WALLET_EXCEPTION_IF(i >= tx.vout.size(), error::wallet_internal_error, "Invalid vout index");
21952195

21962196
// if keys are encrypted, ask for password
2197-
if (m_ask_password == AskPasswordToDecrypt && !m_unattended && !m_watch_only && m_multisig_rescan_k.empty() && !m_background_syncing)
2197+
if (is_key_encryption_enabled() && !m_multisig)
21982198
{
2199-
static critical_section password_lock;
2200-
CRITICAL_REGION_LOCAL(password_lock);
2199+
const boost::lock_guard password_lock(m_encrypt_keys_after_refresh_mutex);
22012200
if (!m_encrypt_keys_after_refresh && !m_processing_background_cache)
22022201
{
22032202
boost::optional<epee::wipeable_string> pwd = m_callback->on_get_password(pool ? "output found in pool" : "output received");
@@ -2556,13 +2555,8 @@ void wallet2::process_new_transaction(const crypto::hash &txid, const cryptonote
25562555
m_pub_keys[tx_scan_info[o].in_ephemeral.pub] = m_transfers.size()-1;
25572556
if (output_tracker_cache)
25582557
(*output_tracker_cache)[std::make_pair(tx.vout[o].amount, td.m_global_output_index)] = m_transfers.size() - 1;
2559-
if (m_multisig)
2560-
{
2561-
THROW_WALLET_EXCEPTION_IF(m_multisig_rescan_k.empty() && !m_multisig_rescan_info.empty(),
2562-
error::wallet_internal_error, "NULL m_multisig_rescan_k");
2563-
if (!m_multisig_rescan_info.empty() && m_multisig_rescan_info.front().size() >= m_transfers.size())
2564-
update_multisig_rescan_info(m_multisig_rescan_k, m_multisig_rescan_info, m_transfers.size() - 1);
2565-
}
2558+
if (is_multisig_key_image_rescan_possible(td.get_public_key()))
2559+
update_multisig_rescan_info(m_transfers.size() - 1);
25662560
LOG_PRINT_L0("Received money: " << print_money(td.amount()) << ", with tx: " << txid);
25672561
if (!ignore_callbacks && 0 != m_callback)
25682562
m_callback->on_money_received(height, txid, tx, td.m_amount, 0, td.m_subaddr_index, spends_one_of_ours(tx), td.m_tx.unlock_time);
@@ -2631,13 +2625,8 @@ void wallet2::process_new_transaction(const crypto::hash &txid, const cryptonote
26312625
}
26322626
if (output_tracker_cache)
26332627
(*output_tracker_cache)[std::make_pair(tx.vout[o].amount, td.m_global_output_index)] = kit->second;
2634-
if (m_multisig)
2635-
{
2636-
THROW_WALLET_EXCEPTION_IF(m_multisig_rescan_k.empty() && !m_multisig_rescan_info.empty(),
2637-
error::wallet_internal_error, "NULL m_multisig_rescan_k");
2638-
if (!m_multisig_rescan_info.empty() && m_multisig_rescan_info.front().size() >= m_transfers.size())
2639-
update_multisig_rescan_info(m_multisig_rescan_k, m_multisig_rescan_info, m_transfers.size() - 1);
2640-
}
2628+
if (is_multisig_key_image_rescan_possible(td.get_public_key()))
2629+
update_multisig_rescan_info(m_transfers.size() - 1);
26412630
THROW_WALLET_EXCEPTION_IF(td.get_public_key() != tx_scan_info[o].in_ephemeral.pub, error::wallet_internal_error, "Inconsistent public keys");
26422631
THROW_WALLET_EXCEPTION_IF(td.m_spent, error::wallet_internal_error, "Inconsistent spent status");
26432632

@@ -3754,6 +3743,7 @@ void wallet2::update_pool_state_by_pool_query(std::vector<std::tuple<cryptonote:
37543743
process_txs.clear();
37553744

37563745
auto keys_reencryptor = epee::misc_utils::create_scope_leave_handler([&, this]() {
3746+
const boost::lock_guard encrypt_keys_after_refresh_lock(m_encrypt_keys_after_refresh_mutex);
37573747
m_encrypt_keys_after_refresh.reset();
37583748
});
37593749

@@ -3827,6 +3817,7 @@ void wallet2::update_pool_state_from_pool_data(bool incremental, const std::vect
38273817
{
38283818
MTRACE("update_pool_state_from_pool_data start");
38293819
auto keys_reencryptor = epee::misc_utils::create_scope_leave_handler([&, this]() {
3820+
const boost::lock_guard encrypt_keys_after_refresh_lock(m_encrypt_keys_after_refresh_mutex);
38303821
m_encrypt_keys_after_refresh.reset();
38313822
});
38323823

@@ -4080,6 +4071,7 @@ void wallet2::refresh(bool trusted_daemon, uint64_t start_height, uint64_t & blo
40804071
start_height = 0;
40814072

40824073
auto keys_reencryptor = epee::misc_utils::create_scope_leave_handler([&, this]() {
4074+
const boost::lock_guard encrypt_keys_after_refresh_lock(m_encrypt_keys_after_refresh_mutex);
40834075
m_encrypt_keys_after_refresh.reset();
40844076
});
40854077

@@ -4249,12 +4241,6 @@ void wallet2::refresh(bool trusted_daemon, uint64_t start_height, uint64_t & blo
42494241
if (m_background_syncing || m_is_background_wallet)
42504242
m_background_sync_data.first_refresh_done = true;
42514243

4252-
m_multisig_rescan_info = std::vector<std::vector<tools::wallet2::multisig_info>>{};
4253-
for (auto &v: m_multisig_rescan_k)
4254-
memwipe(v.data(), v.size() * sizeof(v[0]));
4255-
4256-
m_multisig_rescan_k = std::vector<std::vector<rct::key>>{};
4257-
42584244
LOG_PRINT_L1("Refresh done, blocks received: " << blocks_fetched << ", balance (all accounts): " << print_money(balance_all(false)) << ", unlocked: " << print_money(unlocked_balance_all(false)));
42594245
}
42604246
//----------------------------------------------------------------------------------------------------
@@ -6493,6 +6479,17 @@ void wallet2::load(const std::string& wallet_, const epee::wipeable_string& pass
64936479
}
64946480
}
64956481

6482+
// Populate `m_multsig_rescan_info` and `m_multisig_rescan_k` with existing data in `m_transfers`
6483+
if (m_multisig)
6484+
{
6485+
for (const auto &td : m_transfers)
6486+
{
6487+
const crypto::public_key output_pubkey = td.get_public_key();
6488+
m_multisig_rescan_info[output_pubkey] = td.m_multisig_info;
6489+
m_multisig_rescan_k[output_pubkey] = td.m_multisig_k;
6490+
}
6491+
}
6492+
64966493
cryptonote::block genesis;
64976494
generate_genesis(genesis);
64986495
crypto::hash genesis_hash = get_block_hash(genesis);
@@ -14497,6 +14494,8 @@ cryptonote::blobdata wallet2::export_multisig()
1449714494

1449814495
const crypto::public_key signer = get_multisig_signer_public_key();
1449914496

14497+
MDEBUG("Start export_multisig()");
14498+
1450014499
// For each transfer (output owned by the multisig wallet):
1450114500
// 1) Record the output's partial key image (from the local signer), so other signers can assemble the output's full key image.
1450214501
// 2) Prepare enough signing nonces for one signing attempt with each possible combination of 'threshold' signers
@@ -14544,6 +14543,7 @@ cryptonote::blobdata wallet2::export_multisig()
1454414543
const rct::multisig_kLRki kLRki = get_multisig_kLRki(n, td.m_multisig_k.back());
1454514544
info[n].m_LR.push_back({kLRki.L, kLRki.R});
1454614545
}
14546+
m_multisig_rescan_k[td.get_public_key()] = td.m_multisig_k;
1454714547

1454814548
info[n].m_signer = signer;
1454914549
}
@@ -14562,34 +14562,88 @@ cryptonote::blobdata wallet2::export_multisig()
1456214562
return MULTISIG_EXPORT_FILE_MAGIC + ciphertext;
1456314563
}
1456414564
//----------------------------------------------------------------------------------------------------
14565-
void wallet2::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)
14565+
bool wallet2::is_multisig_key_image_rescan_possible(const crypto::public_key &onetime_pubkey) const
14566+
{
14567+
if (!m_multisig)
14568+
return false; // not multisig
14569+
const auto msinfo_it = m_multisig_rescan_info.find(onetime_pubkey);
14570+
if (msinfo_it == m_multisig_rescan_info.cend())
14571+
return false; // don't have exported multsig info for this output
14572+
else if (msinfo_it->second.size() + 1 < m_multisig_threshold)
14573+
return false; // don't have *enough* exported multsig info for this output
14574+
return true;
14575+
}
14576+
//----------------------------------------------------------------------------------------------------
14577+
void wallet2::update_multisig_rescan_info(const size_t n)
1456614578
{
14579+
CHECK_AND_ASSERT_THROW_MES(m_multisig, "Cannot update multisig rescan info in a non-multisig wallet");
1456714580
CHECK_AND_ASSERT_THROW_MES(n < m_transfers.size(), "Bad index in update_multisig_info");
14568-
CHECK_AND_ASSERT_THROW_MES(multisig_k.size() >= m_transfers.size(), "Mismatched sizes of multisig_k and info");
1456914581

14570-
MDEBUG("update_multisig_rescan_info: updating index " << n);
1457114582
transfer_details &td = m_transfers[n];
14572-
td.m_multisig_info.clear();
14573-
for (const auto &pi: info)
14583+
const crypto::public_key output_pubkey = td.get_public_key();
14584+
const auto msinfo_it = m_multisig_rescan_info.find(output_pubkey);
14585+
CHECK_AND_ASSERT_THROW_MES(msinfo_it != m_multisig_rescan_info.cend(),
14586+
"multisig rescan info missing for output pubkey: " << output_pubkey);
14587+
14588+
if (is_key_encryption_enabled() && !m_processing_background_cache)
1457414589
{
14575-
CHECK_AND_ASSERT_THROW_MES(n < pi.size(), "Bad pi size");
14576-
td.m_multisig_info.push_back(pi[n]);
14590+
const boost::lock_guard password_lock(m_encrypt_keys_after_refresh_mutex);
14591+
if (!m_encrypt_keys_after_refresh)
14592+
{
14593+
boost::optional<epee::wipeable_string> pwd = m_callback->on_get_password("output received");
14594+
THROW_WALLET_EXCEPTION_IF(!pwd, error::password_needed, tr("Password is needed to compute key image for incoming monero"));
14595+
THROW_WALLET_EXCEPTION_IF(!verify_password(*pwd), error::password_needed, tr("Invalid password: password is needed to compute key image for incoming monero"));
14596+
m_encrypt_keys_after_refresh.reset(new wallet_keys_unlocker(*this, &*pwd));
14597+
}
14598+
}
14599+
14600+
MDEBUG("update_multisig_rescan_info: updating index " << n);
14601+
const crypto::key_image old_key_image = td.m_key_image;
14602+
std::vector<multisig_info> old_multisig_info = msinfo_it->second;
14603+
std::swap(old_multisig_info, td.m_multisig_info);
14604+
try
14605+
{
14606+
// try update key images and multisig exchange infos
14607+
td.m_key_image = get_multisig_composite_key_image(n); // may throw
14608+
m_key_images.erase(old_key_image);
14609+
td.m_key_image_known = true;
14610+
td.m_key_image_request = false;
14611+
td.m_key_image_partial = false;
14612+
m_key_images[td.m_key_image] = n;
14613+
14614+
// update local multisig keys
14615+
const auto mskey_it = m_multisig_rescan_k.find(output_pubkey);
14616+
if (mskey_it != m_multisig_rescan_k.cend())
14617+
{
14618+
memwipe(td.m_multisig_k.data(), td.m_multisig_k.size() * sizeof(td.m_multisig_k[0]));
14619+
td.m_multisig_k = mskey_it->second;
14620+
}
14621+
}
14622+
catch (const std::exception &e)
14623+
{
14624+
MERROR("Encountered exception while attempting to build composite multisig key image, reverting state: "
14625+
<< e.what());
14626+
std::swap(old_multisig_info, td.m_multisig_info);
1457714627
}
14578-
m_key_images.erase(td.m_key_image);
14579-
td.m_key_image = get_multisig_composite_key_image(n);
14580-
td.m_key_image_known = true;
14581-
td.m_key_image_request = false;
14582-
td.m_key_image_partial = false;
14583-
td.m_multisig_k = multisig_k[n];
14584-
m_key_images[td.m_key_image] = n;
1458514628
}
1458614629
//----------------------------------------------------------------------------------------------------
1458714630
size_t wallet2::import_multisig(std::vector<cryptonote::blobdata> blobs)
1458814631
{
1458914632
CHECK_AND_ASSERT_THROW_MES(m_multisig, "Wallet is not multisig");
14633+
CHECK_AND_ASSERT_THROW_MES(!m_processing_background_cache,
14634+
"Multisig wallet is marked as proccesing background cache");
14635+
14636+
// Set `m_processing_background_cache=true` just long enough for calls to
14637+
// `update_multisig_rescan_info()` to not ask for the password.
14638+
m_processing_background_cache = true;
14639+
const auto done_processing = epee::misc_utils::create_scope_leave_handler([this]() {
14640+
m_processing_background_cache = false;
14641+
});
1459014642

14591-
if (!m_multisig_rescan_k.empty() && !m_multisig_rescan_info.empty())
14592-
refresh(false);
14643+
// Unconditionally refresh to attempt to capture latest on-chain received enotes that other
14644+
// participants have seen and we have not.
14645+
try { refresh(false); }
14646+
catch (...) {}
1459314647

1459414648
std::unordered_set<crypto::public_key> seen;
1459514649
for (cryptonote::blobdata &data: blobs)
@@ -14614,6 +14668,9 @@ size_t wallet2::import_multisig(std::vector<cryptonote::blobdata> blobs)
1461414668
MINFO("Multisig info from this wallet ignored");
1461514669
continue;
1461614670
}
14671+
const bool is_signer_recognized = std::find(m_multisig_signers.cbegin(), m_multisig_signers.cend(), signer)
14672+
!= m_multisig_signers.cend();
14673+
CHECK_AND_ASSERT_THROW_MES(is_signer_recognized, "Signer is not a member of this multisig wallet");
1461714674
if (seen.find(signer) != seen.end())
1461814675
{
1461914676
MINFO("Duplicate multisig info ignored");
@@ -14648,63 +14705,67 @@ size_t wallet2::import_multisig(std::vector<cryptonote::blobdata> blobs)
1464814705
}
1464914706
}
1465014707

14708+
// Insert multisig infos into `m_multisig_rescan_info` based on current signer and output pubkeys in `m_transfers`.
14709+
// We are forced to guess because the output pubkey that a multisig export is tied to isn't explicitly provided in
14710+
// message. This causes problems if the order is different from when a peer exports to when we import (like during a
14711+
// reorg). However, this isn't really fixable without introducing backwards-incompatible changes to the messaging
14712+
// protocol or the API for multisig app developers.
1465114713
MINFO(boost::format("%u outputs found") % boost::lexical_cast<std::string>(i.size()));
14652-
m_multisig_rescan_info.push_back(std::move(i));
14653-
}
14654-
14655-
CHECK_AND_ASSERT_THROW_MES(m_multisig_rescan_info.size() + 1 <= m_multisig_signers.size() && m_multisig_rescan_info.size() + 1 >= m_multisig_threshold, "Wrong number of multisig sources");
14656-
14657-
m_multisig_rescan_k.reserve(m_transfers.size());
14658-
for (const auto &td: m_transfers)
14659-
m_multisig_rescan_k.push_back(td.m_multisig_k);
14660-
14661-
// how many outputs we're going to update
14662-
size_t n_outputs = m_transfers.size();
14663-
for (const auto &pi: m_multisig_rescan_info)
14664-
if (pi.size() < n_outputs)
14665-
n_outputs = pi.size();
14666-
14667-
if (n_outputs == 0)
14668-
return 0;
14669-
14670-
// check signers are consistent
14671-
for (const auto &pi: m_multisig_rescan_info)
14672-
{
14673-
CHECK_AND_ASSERT_THROW_MES(std::find(m_multisig_signers.begin(), m_multisig_signers.end(), pi[0].m_signer) != m_multisig_signers.end(),
14674-
"Signer is not a member of this multisig wallet");
14675-
for (size_t n = 1; n < n_outputs; ++n)
14676-
CHECK_AND_ASSERT_THROW_MES(pi[n].m_signer == pi[0].m_signer, "Mismatched signers in imported multisig info");
14714+
for (std::size_t transfers_idx = 0; transfers_idx < i.size(); ++transfers_idx)
14715+
{
14716+
if (transfers_idx >= m_transfers.size())
14717+
break;
14718+
const crypto::public_key assumed_onetime_pubkey = m_transfers.at(transfers_idx).get_public_key();
14719+
auto &ms_info_for_otpk = m_multisig_rescan_info[assumed_onetime_pubkey];
14720+
const auto signer_it = std::find_if(ms_info_for_otpk.begin(), ms_info_for_otpk.end(),
14721+
[&signer](const auto &x) { return x.m_signer == signer; });
14722+
// if multisig info for this signer and OTA was already present, replace it, else append it
14723+
if (signer_it != ms_info_for_otpk.end())
14724+
*signer_it = i.at(transfers_idx);
14725+
else
14726+
ms_info_for_otpk.push_back(i.at(transfers_idx));
14727+
}
1467714728
}
1467814729

14679-
// trim data we don't have info for from all participants
14680-
for (auto &pi: m_multisig_rescan_info)
14681-
pi.resize(n_outputs);
14682-
1468314730
// sort by signer
14684-
if (!m_multisig_rescan_info.empty() && !m_multisig_rescan_info.front().empty())
14731+
for (auto &p : m_multisig_rescan_info)
1468514732
{
14686-
std::sort(m_multisig_rescan_info.begin(), m_multisig_rescan_info.end(), [](const std::vector<tools::wallet2::multisig_info> &i0, const std::vector<tools::wallet2::multisig_info> &i1){ return memcmp(&i0[0].m_signer, &i1[0].m_signer, sizeof(i0[0].m_signer)) < 0; });
14733+
std::sort(p.second.begin(), p.second.end(),
14734+
[](const multisig_info &i0, const multisig_info &i1) { return i0.m_signer < i1.m_signer; });
1468714735
}
1468814736

14689-
// first pass to determine where to detach the blockchain
14690-
for (size_t n = 0; n < n_outputs; ++n)
14737+
// First pass to determine where to detach the blockchain.
14738+
// This allows us to find where enotes were spent on-chain next refresh, even if we have scanned past that point.
14739+
for (const transfer_details &td : m_transfers)
1469114740
{
14692-
const transfer_details &td = m_transfers[n];
1469314741
if (!td.m_key_image_partial)
1469414742
continue;
1469514743
MINFO("Multisig info importing from block height " << td.m_block_height);
1469614744
handle_reorg(td.m_block_height);
1469714745
break;
1469814746
}
1469914747

14700-
for (size_t n = 0; n < n_outputs && n < m_transfers.size(); ++n)
14748+
// Preliminary round of updating multisig infos. This updates multisig info for outputs which
14749+
// have already been imported and their key images are calculated. These won't be touched in the
14750+
// following refresh.
14751+
for (std::size_t transfer_idx = 0; transfer_idx < m_transfers.size(); ++transfer_idx)
1470114752
{
14702-
update_multisig_rescan_info(m_multisig_rescan_k, m_multisig_rescan_info, n);
14753+
const transfer_details &td = m_transfers.at(transfer_idx);
14754+
if (is_multisig_key_image_rescan_possible(td.get_public_key()))
14755+
update_multisig_rescan_info(transfer_idx);
1470314756
}
1470414757

14705-
14758+
// refresh to find where enotes are spent and fill in key image details
1470614759
refresh(false);
1470714760

14761+
// count number of known key images after refresh
14762+
std::size_t n_outputs = 0;
14763+
for (const transfer_details &td : m_transfers)
14764+
{
14765+
if (td.m_key_image_known && !td.m_key_image_partial)
14766+
++n_outputs;
14767+
}
14768+
1470814769
return n_outputs;
1470914770
}
1471014771
//----------------------------------------------------------------------------------------------------

src/wallet/wallet2.h

Lines changed: 20 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1865,7 +1865,21 @@ namespace tools
18651865
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;
18661866
rct::multisig_kLRki get_multisig_kLRki(size_t n, const rct::key &k) const;
18671867
void get_multisig_k(size_t idx, const std::unordered_set<rct::key> &used_L, rct::key &nonce);
1868-
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);
1868+
/**
1869+
* brief: determine whether is it possible that we have enough multisig messages to attempt building composite key image
1870+
*/
1871+
bool is_multisig_key_image_rescan_possible(const crypto::public_key &onetime_pubkey) const;
1872+
/**
1873+
* brief: build composite key image, multisig exchange infos, and other metadata for transfer at `m_transfers[n]`
1874+
* param: n -
1875+
* throw: if not a multisig wallet, `n` is out of bounds, missing necessary info, or key image building fails
1876+
*
1877+
* Populates the transfer entry using `m_multisig_rescan_info` and `m_multisig_rescan_k`.
1878+
* Attempts to revert update on failure. Requests for password from callback to unlock multisig
1879+
* keys and assigns to `m_encrypt_keys_after_refresh` if key encryption is enabled for this
1880+
* wallet and `m_processing_background_cache` is false.
1881+
*/
1882+
void update_multisig_rescan_info(const size_t n);
18691883
bool add_rings(const crypto::chacha_key &key, const cryptonote::transaction_prefix &tx);
18701884
bool add_rings(const cryptonote::transaction_prefix &tx);
18711885
bool remove_rings(const cryptonote::transaction_prefix &tx);
@@ -1942,8 +1956,10 @@ namespace tools
19421956
std::vector<tools::wallet2::address_book_row> m_address_book;
19431957
std::pair<std::map<std::string, std::string>, std::vector<std::string>> m_account_tags;
19441958
uint64_t m_upper_transaction_weight_limit; //TODO: auto-calc this value or request from daemon, now use some fixed value
1945-
std::vector<std::vector<tools::wallet2::multisig_info>> m_multisig_rescan_info;
1946-
std::vector<std::vector<rct::key>> m_multisig_rescan_k;
1959+
/// map of outut public key to list of multisig infos, one per other each other signer
1960+
std::unordered_map<crypto::public_key, std::vector<tools::wallet2::multisig_info>> m_multisig_rescan_info;
1961+
/// map of output public key to list of multisig keys for that enote
1962+
std::unordered_map<crypto::public_key, std::vector<rct::key>> m_multisig_rescan_k;
19471963
std::unordered_map<crypto::public_key, crypto::key_image> m_cold_key_images;
19481964

19491965
std::atomic<bool> m_run;
@@ -2036,6 +2052,7 @@ namespace tools
20362052
crypto::chacha_key m_cache_key;
20372053
boost::optional<crypto::chacha_key> m_custom_background_key = boost::none;
20382054
std::shared_ptr<wallet_keys_unlocker> m_encrypt_keys_after_refresh;
2055+
boost::mutex m_encrypt_keys_after_refresh_mutex;
20392056

20402057
bool m_unattended;
20412058
bool m_devices_registered;

0 commit comments

Comments
 (0)