aboutsummaryrefslogtreecommitdiff
path: root/src/wallet/wallet2.cpp
diff options
context:
space:
mode:
authormoneromooo-monero <moneromooo-monero@users.noreply.github.com>2017-06-03 22:34:26 +0100
committermoneromooo-monero <moneromooo-monero@users.noreply.github.com>2017-12-17 16:11:57 +0000
commit4c313324b1c80148dff1a8099aa26c51ab6c7e3a (patch)
tree95af65fc85411e854bb88f702c1b8ef3e6dba896 /src/wallet/wallet2.cpp
parentwallet: add multisig key generation (diff)
downloadmonero-4c313324b1c80148dff1a8099aa26c51ab6c7e3a.tar.xz
Add N/N multisig tx generation and signing
Scheme by luigi1111: Multisig for RingCT on Monero 2 of 2 User A (coordinator): Spendkey b,B Viewkey a,A (shared) User B: Spendkey c,C Viewkey a,A (shared) Public Address: C+B, A Both have their own watch only wallet via C+B, a A will coordinate spending process (though B could easily as well, coordinator is more needed for more participants) A and B watch for incoming outputs B creates "half" key images for discovered output D: I2_D = (Hs(aR)+c) * Hp(D) B also creates 1.5 random keypairs (one scalar and 2 pubkeys; one on base G and one on base Hp(D)) for each output, storing the scalar(k) (linked to D), and sending the pubkeys with I2_D. A also creates "half" key images: I1_D = (Hs(aR)+b) * Hp(D) Then I_D = I1_D + I2_D Having I_D allows A to check spent status of course, but more importantly allows A to actually build a transaction prefix (and thus transaction). A builds the transaction until most of the way through MLSAG_Gen, adding the 2 pubkeys (per input) provided with I2_D to his own generated ones where they are needed (secret row L, R). At this point, A has a mostly completed transaction (but with an invalid/incomplete signature). A sends over the tx and includes r, which allows B (with the recipient's address) to verify the destination and amount (by reconstructing the stealth address and decoding ecdhInfo). B then finishes the signature by computing ss[secret_index][0] = ss[secret_index][0] + k - cc[secret_index]*c (secret indices need to be passed as well). B can then broadcast the tx, or send it back to A for broadcasting. Once B has completed the signing (and verified the tx to be valid), he can add the full I_D to his cache, allowing him to verify spent status as well. NOTE: A and B *must* present key A and B to each other with a valid signature proving they know a and b respectively. Otherwise, trickery like the following becomes possible: A creates viewkey a,A, spendkey b,B, and sends a,A,B to B. B creates a fake key C = zG - B. B sends C back to A. The combined spendkey C+B then equals zG, allowing B to spend funds at any time! The signature fixes this, because B does not know a c corresponding to C (and thus can't produce a signature). 2 of 3 User A (coordinator) Shared viewkey a,A "spendkey" j,J User B "spendkey" k,K User C "spendkey" m,M A collects K and M from B and C B collects J and M from A and C C collects J and K from A and B A computes N = nG, n = Hs(jK) A computes O = oG, o = Hs(jM) B anc C compute P = pG, p = Hs(kM) || Hs(mK) B and C can also compute N and O respectively if they wish to be able to coordinate Address: N+O+P, A The rest follows as above. The coordinator possesses 2 of 3 needed keys; he can get the other needed part of the signature/key images from either of the other two. Alternatively, if secure communication exists between parties: A gives j to B B gives k to C C gives m to A Address: J+K+M, A 3 of 3 Identical to 2 of 2, except the coordinator must collect the key images from both of the others. The transaction must also be passed an additional hop: A -> B -> C (or A -> C -> B), who can then broadcast it or send it back to A. N-1 of N Generally the same as 2 of 3, except participants need to be arranged in a ring to pass their keys around (using either the secure or insecure method). For example (ignoring viewkey so letters line up): [4 of 5] User: spendkey A: a B: b C: c D: d E: e a -> B, b -> C, c -> D, d -> E, e -> A Order of signing does not matter, it just must reach n-1 users. A "remaining keys" list must be passed around with the transaction so the signers know if they should use 1 or both keys. Collecting key image parts becomes a little messy, but basically every wallet sends over both of their parts with a tag for each. Thia way the coordinating wallet can keep track of which images have been added and which wallet they come from. Reasoning: 1. The key images must be added only once (coordinator will get key images for key a from both A and B, he must add only one to get the proper key actual key image) 2. The coordinator must keep track of which helper pubkeys came from which wallet (discussed in 2 of 2 section). The coordinator must choose only one set to use, then include his choice in the "remaining keys" list so the other wallets know which of their keys to use. You can generalize it further to N-2 of N or even M of N, but I'm not sure there's legitimate demand to justify the complexity. It might also be straightforward enough to support with minimal changes from N-1 format. You basically just give each user additional keys for each additional "-1" you desire. N-2 would be 3 keys per user, N-3 4 keys, etc. The process is somewhat cumbersome: To create a N/N multisig wallet: - each participant creates a normal wallet - each participant runs "prepare_multisig", and sends the resulting string to every other participant - each participant runs "make_multisig N A B C D...", with N being the threshold and A B C D... being the strings received from other participants (the threshold must currently equal N) As txes are received, participants' wallets will need to synchronize so that those new outputs may be spent: - each participant runs "export_multisig FILENAME", and sends the FILENAME file to every other participant - each participant runs "import_multisig A B C D...", with A B C D... being the filenames received from other participants Then, a transaction may be initiated: - one of the participants runs "transfer ADDRESS AMOUNT" - this partly signed transaction will be written to the "multisig_monero_tx" file - the initiator sends this file to another participant - that other participant runs "sign_multisig multisig_monero_tx" - the resulting transaction is written to the "multisig_monero_tx" file again - if the threshold was not reached, the file must be sent to another participant, until enough have signed - the last participant to sign runs "submit_multisig multisig_monero_tx" to relay the transaction to the Monero network
Diffstat (limited to '')
-rw-r--r--src/wallet/wallet2.cpp575
1 files changed, 515 insertions, 60 deletions
diff --git a/src/wallet/wallet2.cpp b/src/wallet/wallet2.cpp
index 4ad527423..af48e711e 100644
--- a/src/wallet/wallet2.cpp
+++ b/src/wallet/wallet2.cpp
@@ -56,6 +56,7 @@ using namespace epee;
#include "mnemonics/electrum-words.h"
#include "common/i18n.h"
#include "common/util.h"
+#include "common/apply_permutation.h"
#include "rapidjson/document.h"
#include "rapidjson/writer.h"
#include "rapidjson/stringbuffer.h"
@@ -87,6 +88,7 @@ using namespace cryptonote;
#define UNSIGNED_TX_PREFIX "Monero unsigned tx set\004"
#define SIGNED_TX_PREFIX "Monero signed tx set\004"
+#define MULTISIG_UNSIGNED_TX_PREFIX "Monero multisig unsigned tx set\001"
#define RECENT_OUTPUT_RATIO (0.5) // 50% of outputs are from the recent zone
#define RECENT_OUTPUT_ZONE ((time_t)(1.8 * 86400)) // last 1.8 day makes up the recent zone (taken from monerolink.pdf, Miller et al)
@@ -524,6 +526,69 @@ uint8_t get_bulletproof_fork(bool testnet)
return 255; // TODO
}
+bool generate_key_image_helper_old(const account_keys& ack, const crypto::public_key& tx_public_key, size_t real_output_index, keypair& in_ephemeral, crypto::key_image& ki)
+{
+ crypto::key_derivation recv_derivation = AUTO_VAL_INIT(recv_derivation);
+ bool r = crypto::generate_key_derivation(tx_public_key, ack.m_view_secret_key, recv_derivation);
+ CHECK_AND_ASSERT_MES(r, false, "key image helper: failed to generate_key_derivation(" << tx_public_key << ", " << ack.m_view_secret_key << ")");
+
+ r = crypto::derive_public_key(recv_derivation, real_output_index, ack.m_account_address.m_spend_public_key, in_ephemeral.pub);
+ CHECK_AND_ASSERT_MES(r, false, "key image helper: failed to derive_public_key(" << recv_derivation << ", " << real_output_index << ", " << ack.m_account_address.m_spend_public_key << ")");
+
+ crypto::derive_secret_key(recv_derivation, real_output_index, ack.m_spend_secret_key, in_ephemeral.sec);
+
+ crypto::generate_key_image(in_ephemeral.pub, in_ephemeral.sec, ki);
+ return true;
+}
+
+bool wallet_generate_key_image_helper_old(const cryptonote::account_keys& ack, const crypto::public_key& tx_public_key, size_t real_output_index, cryptonote::keypair& in_ephemeral, crypto::key_image& ki, bool multisig_export = false)
+{
+ if (!generate_key_image_helper_old(ack, tx_public_key, real_output_index, in_ephemeral, ki))
+ return false;
+ if (multisig_export)
+ {
+ // we got the ephemeral keypair, but the key image isn't right as it's done as per our private spend key, which is multisig
+ crypto::generate_key_image(in_ephemeral.pub, ack.m_spend_secret_key, ki);
+ }
+ return true;
+}
+
+crypto::hash8 get_short_payment_id(const tools::wallet2::pending_tx &ptx)
+{
+ crypto::hash8 payment_id8 = null_hash8;
+ std::vector<tx_extra_field> tx_extra_fields;
+ if(!parse_tx_extra(ptx.tx.extra, tx_extra_fields))
+ return payment_id8;
+ cryptonote::tx_extra_nonce extra_nonce;
+ if (find_tx_extra_field_by_type(tx_extra_fields, extra_nonce))
+ {
+ if(get_encrypted_payment_id_from_tx_extra_nonce(extra_nonce.nonce, payment_id8))
+ {
+ decrypt_payment_id(payment_id8, ptx.dests[0].addr.m_view_public_key, ptx.tx_key);
+ }
+ }
+ return payment_id8;
+}
+
+tools::wallet2::tx_construction_data get_construction_data_with_decrypted_short_payment_id(const tools::wallet2::pending_tx &ptx)
+{
+ tools::wallet2::tx_construction_data construction_data = ptx.construction_data;
+ crypto::hash8 payment_id = get_short_payment_id(ptx);
+ if (payment_id != null_hash8)
+ {
+ // Remove encrypted
+ remove_field_from_tx_extra(construction_data.extra, typeid(cryptonote::tx_extra_nonce));
+ // Add decrypted
+ std::string extra_nonce;
+ set_encrypted_payment_id_to_tx_extra_nonce(extra_nonce, payment_id);
+ THROW_WALLET_EXCEPTION_IF(!add_extra_nonce_to_tx_extra(construction_data.extra, extra_nonce),
+ tools::error::wallet_internal_error, "Failed to add decrypted payment id to tx extra");
+ LOG_PRINT_L1("Decrypted payment ID: " << payment_id);
+ }
+ return construction_data;
+}
+
+ //-----------------------------------------------------------------
} //namespace
namespace tools
@@ -846,7 +911,15 @@ static uint64_t decodeRct(const rct::rctSig & rv, const crypto::key_derivation &
//----------------------------------------------------------------------------------------------------
void wallet2::scan_output(const cryptonote::account_keys &keys, const cryptonote::transaction &tx, const crypto::public_key &tx_pub_key, size_t i, tx_scan_info_t &tx_scan_info, int &num_vouts_received, std::unordered_map<cryptonote::subaddress_index, uint64_t> &tx_money_got_in_outs, std::vector<size_t> &outs)
{
- bool r = cryptonote::generate_key_image_helper_precomp(keys, boost::get<cryptonote::txout_to_key>(tx.vout[i].target).key, tx_scan_info.received->derivation, i, tx_scan_info.received->index, tx_scan_info.in_ephemeral, tx_scan_info.ki);
+ bool r;
+ if (m_multisig)
+ {
+ r = wallet_generate_key_image_helper_old(keys, tx_pub_key, i, tx_scan_info.in_ephemeral, tx_scan_info.ki);
+ }
+ else
+ {
+ r = cryptonote::generate_key_image_helper_precomp(keys, boost::get<cryptonote::txout_to_key>(tx.vout[i].target).key, tx_scan_info.received->derivation, i, tx_scan_info.received->index, tx_scan_info.in_ephemeral, tx_scan_info.ki);
+ }
THROW_WALLET_EXCEPTION_IF(!r, error::wallet_internal_error, "Failed to generate key image");
THROW_WALLET_EXCEPTION_IF(tx_scan_info.in_ephemeral.pub != boost::get<cryptonote::txout_to_key>(tx.vout[i].target).key,
error::wallet_internal_error, "key_image generated ephemeral public key not matched with output_key");
@@ -1018,7 +1091,8 @@ void wallet2::process_new_transaction(const crypto::hash &txid, const cryptonote
td.m_tx = (const cryptonote::transaction_prefix&)tx;
td.m_txid = txid;
td.m_key_image = tx_scan_info[o].ki;
- td.m_key_image_known = !m_watch_only;
+ td.m_key_image_known = !m_watch_only && !m_multisig;
+ td.m_key_image_partial = m_multisig;
td.m_amount = tx.vout[o].amount;
td.m_pk_index = pk_index - 1;
td.m_subaddr_index = tx_scan_info[o].received->index;
@@ -1040,8 +1114,18 @@ void wallet2::process_new_transaction(const crypto::hash &txid, const cryptonote
td.m_rct = false;
}
set_unspent(m_transfers.size()-1);
- m_key_images[td.m_key_image] = m_transfers.size()-1;
+ if (!m_multisig)
+ m_key_images[td.m_key_image] = m_transfers.size()-1;
m_pub_keys[tx_scan_info[o].in_ephemeral.pub] = m_transfers.size()-1;
+ if (m_multisig)
+ {
+ THROW_WALLET_EXCEPTION_IF(!m_multisig_rescan_k && m_multisig_rescan_info,
+ error::wallet_internal_error, "NULL m_multisig_rescan_k");
+ if (m_multisig_rescan_info && m_multisig_rescan_info->front().size() >= m_transfers.size())
+ update_multisig_rescan_info(*m_multisig_rescan_k, *m_multisig_rescan_info, m_transfers.size() - 1);
+ else
+ td.m_multisig_k = rct::skGen();
+ }
LOG_PRINT_L0("Received money: " << print_money(td.amount()) << ", with tx: " << txid);
if (0 != m_callback)
m_callback->on_money_received(height, txid, tx, td.m_amount, td.m_subaddr_index);
@@ -1090,6 +1174,15 @@ void wallet2::process_new_transaction(const crypto::hash &txid, const cryptonote
td.m_mask = rct::identity();
td.m_rct = false;
}
+ if (m_multisig)
+ {
+ THROW_WALLET_EXCEPTION_IF(!m_multisig_rescan_k && m_multisig_rescan_info,
+ error::wallet_internal_error, "NULL m_multisig_rescan_k");
+ if (m_multisig_rescan_info && m_multisig_rescan_info->front().size() >= m_transfers.size())
+ update_multisig_rescan_info(*m_multisig_rescan_k, *m_multisig_rescan_info, m_transfers.size() - 1);
+ else
+ td.m_multisig_k = rct::skGen();
+ }
THROW_WALLET_EXCEPTION_IF(td.get_public_key() != tx_scan_info[o].in_ephemeral.pub, error::wallet_internal_error, "Inconsistent public keys");
THROW_WALLET_EXCEPTION_IF(td.m_spent, error::wallet_internal_error, "Inconsistent spent status");
@@ -2083,8 +2176,10 @@ void wallet2::detach_blockchain(uint64_t height)
for(size_t i = i_start; i!= m_transfers.size();i++)
{
+ if (!m_transfers[i].m_key_image_known || m_transfers[i].m_key_image_partial)
+ continue;
auto it_ki = m_key_images.find(m_transfers[i].m_key_image);
- THROW_WALLET_EXCEPTION_IF(it_ki == m_key_images.end(), error::wallet_internal_error, "key image not found");
+ THROW_WALLET_EXCEPTION_IF(it_ki == m_key_images.end(), error::wallet_internal_error, "key image not found: index " + std::to_string(i) + ", ki " + epee::string_tools::pod_to_hex(m_transfers[i].m_key_image) + ", " + std::to_string(m_key_images.size()) + " key images known");
m_key_images.erase(it_ki);
}
@@ -2437,7 +2532,7 @@ bool wallet2::verify_password(const epee::wipeable_string& password) const
* \brief verify password for specified wallet keys file.
* \param keys_file_name Keys file to verify password for
* \param password Password to verify
- * \param watch_only If set = only verify view keys, otherwise also spend keys
+ * \param no_spend_key If set = only verify view keys, otherwise also spend keys
* \return true if password is correct
*
* for verification only
@@ -2653,8 +2748,8 @@ void wallet2::make_multisig(const epee::wipeable_string &password,
{
CHECK_AND_ASSERT_THROW_MES(!view_keys.empty(), "empty view keys");
CHECK_AND_ASSERT_THROW_MES(view_keys.size() == spend_keys.size(), "Mismatched view/spend key sizes");
- CHECK_AND_ASSERT_THROW_MES(threshold > 0 && threshold <= spend_keys.size() + 1, "Invalid threshold");
- CHECK_AND_ASSERT_THROW_MES(threshold == spend_keys.size() || threshold == spend_keys.size() + 1, "Unsupported threshold case");
+ CHECK_AND_ASSERT_THROW_MES(threshold > 1 && threshold <= spend_keys.size() + 1, "Invalid threshold");
+ CHECK_AND_ASSERT_THROW_MES(/*threshold == spend_keys.size() ||*/ threshold == spend_keys.size() + 1, "Unsupported threshold case");
clear();
@@ -2669,19 +2764,20 @@ void wallet2::make_multisig(const epee::wipeable_string &password,
else
{
// the multisig spend public key is the sum of keys derived from all spend public keys
- const rct::key spend_skey = rct::sk2rct(get_account().get_keys().m_spend_secret_key);
+ const rct::key spend_skey = rct::sk2rct(get_account().get_keys().m_spend_secret_key); // WRONG
for (const auto &k: spend_keys)
{
- rct::addKeys(spend_pkey, spend_pkey, rct::scalarmultBase(rct::cn_fast_hash(rct::scalarmultKey(rct::pk2rct(k), spend_skey))));
+ rct::addKeys(spend_pkey, spend_pkey, rct::scalarmultBase(rct::hash_to_scalar(rct::scalarmultKey(rct::pk2rct(k), spend_skey))));
}
}
// the multisig view key is shared by all, make one all can derive
MINFO("Creating view key...");
- rct::key view_skey = rct::sk2rct(get_account().get_keys().m_view_secret_key);
+ crypto::hash hash;
+ crypto::cn_fast_hash(&get_account().get_keys().m_view_secret_key, sizeof(crypto::secret_key), hash);
+ rct::key view_skey = rct::hash2rct(hash);
for (const auto &k: view_keys)
sc_add(view_skey.bytes, view_skey.bytes, rct::sk2rct(k).bytes);
- sc_reduce32(view_skey.bytes);
MINFO("Creating multisig address...");
CHECK_AND_ASSERT_THROW_MES(m_account.make_multisig(rct::rct2sk(view_skey), rct::rct2pk(spend_pkey)),
@@ -2712,13 +2808,14 @@ std::string wallet2::get_multisig_info() const
// It's a signed package of private view key and public spend key
const crypto::secret_key &skey = get_account().get_keys().m_view_secret_key;
const crypto::public_key &pkey = get_account().get_keys().m_account_address.m_spend_public_key;
+ crypto::hash hash;
std::string data;
- data += std::string((const char *)&skey, sizeof(crypto::secret_key));
+ crypto::cn_fast_hash(&skey, sizeof(crypto::secret_key), hash);
+ data += std::string((const char *)&hash, sizeof(crypto::hash));
data += std::string((const char *)&pkey, sizeof(crypto::public_key));
data.resize(data.size() + sizeof(crypto::signature));
- crypto::hash hash;
crypto::cn_fast_hash(data.data(), data.size() - sizeof(signature), hash);
crypto::signature &signature = *(crypto::signature*)&data[data.size() - sizeof(crypto::signature)];
crypto::generate_signature(hash, pkey, get_account().get_keys().m_spend_secret_key, signature);
@@ -2775,6 +2872,16 @@ bool wallet2::multisig(uint32_t *threshold, uint32_t *total) const
return true;
}
+bool wallet2::has_multisig_partial_key_images() const
+{
+ if (!m_multisig)
+ return false;
+ for (const auto &td: m_transfers)
+ if (td.m_key_image_partial)
+ return true;
+ return false;
+}
+
/*!
* \brief Rewrites to the wallet file for wallet upgrade (doesn't generate key, assumes it's already there)
* \param wallet_name Name of wallet file (should exist)
@@ -3355,7 +3462,7 @@ void wallet2::rescan_spent()
{
transfer_details& td = m_transfers[i];
// a view wallet may not know about key images
- if (!td.m_key_image_known)
+ if (!td.m_key_image_known || td.m_key_image_partial)
continue;
if (td.m_spent != (spent_status[i] != COMMAND_RPC_IS_KEY_IMAGE_SPENT::UNSPENT))
{
@@ -3695,23 +3802,6 @@ crypto::hash wallet2::get_payment_id(const pending_tx &ptx) const
return payment_id;
}
-crypto::hash8 wallet2::get_short_payment_id(const pending_tx &ptx) const
-{
- crypto::hash8 payment_id8 = null_hash8;
- std::vector<tx_extra_field> tx_extra_fields;
- if(!parse_tx_extra(ptx.tx.extra, tx_extra_fields))
- return payment_id8;
- cryptonote::tx_extra_nonce extra_nonce;
- if (find_tx_extra_field_by_type(tx_extra_fields, extra_nonce))
- {
- if(get_encrypted_payment_id_from_tx_extra_nonce(extra_nonce.nonce, payment_id8))
- {
- decrypt_payment_id(payment_id8, ptx.dests[0].addr.m_view_public_key, ptx.tx_key);
- }
- }
- return payment_id8;
-}
-
//----------------------------------------------------------------------------------------------------
// take a pending tx and actually send it to the daemon
void wallet2::commit_tx(pending_tx& ptx)
@@ -3779,6 +3869,10 @@ void wallet2::commit_tx(pending_tx& ptx)
set_spent(idx, 0);
}
+ // tx generated, get rid of used k values
+ for (size_t idx: ptx.selected_transfers)
+ m_transfers[idx].m_multisig_k = rct::zero();
+
//fee includes dust if dust policy specified it.
LOG_PRINT_L1("Transaction successfully sent. <" << txid << ">" << ENDL
<< "Commission: " << print_money(ptx.fee) << " (dust sent to dust addr: " << print_money((ptx.dust_added_to_fee ? 0 : ptx.dust)) << ")" << ENDL
@@ -3801,27 +3895,10 @@ bool wallet2::save_tx(const std::vector<pending_tx>& ptx_vector, const std::stri
unsigned_tx_set txs;
for (auto &tx: ptx_vector)
{
- tx_construction_data construction_data = tx.construction_data;
// Short payment id is encrypted with tx_key.
// Since sign_tx() generates new tx_keys and encrypts the payment id, we need to save the decrypted payment ID
- // Get decrypted payment id from pending_tx
- crypto::hash8 payment_id = get_short_payment_id(tx);
- if (payment_id != null_hash8)
- {
- // Remove encrypted
- remove_field_from_tx_extra(construction_data.extra, typeid(cryptonote::tx_extra_nonce));
- // Add decrypted
- std::string extra_nonce;
- set_encrypted_payment_id_to_tx_extra_nonce(extra_nonce, payment_id);
- if (!add_extra_nonce_to_tx_extra(construction_data.extra, extra_nonce))
- {
- LOG_ERROR("Failed to add decrypted payment id to tx extra");
- return false;
- }
- LOG_PRINT_L1("Decrypted payment ID: " << payment_id);
- }
// Save tx construction_data to unsigned_tx_set
- txs.txes.push_back(construction_data);
+ txs.txes.push_back(get_construction_data_with_decrypted_short_payment_id(tx));
}
txs.transfers = m_transfers;
@@ -3942,7 +4019,8 @@ bool wallet2::sign_tx(unsigned_tx_set &exported_txs, const std::string &signed_f
bool bulletproof = sd.use_rct && !ptx.tx.rct_signatures.p.bulletproofs.empty();
crypto::secret_key tx_key;
std::vector<crypto::secret_key> additional_tx_keys;
- bool r = cryptonote::construct_tx_and_get_tx_key(m_account.get_keys(), m_subaddresses, sd.sources, sd.splitted_dsts, sd.change_dts.addr, sd.extra, ptx.tx, sd.unlock_time, tx_key, additional_tx_keys, sd.use_rct, bulletproof);
+ rct::multisig_out msout;
+ bool r = cryptonote::construct_tx_and_get_tx_key(m_account.get_keys(), m_subaddresses, sd.sources, sd.splitted_dsts, sd.change_dts.addr, sd.extra, ptx.tx, sd.unlock_time, tx_key, additional_tx_keys, sd.use_rct, bulletproof, m_multisig ? &msout : NULL);
THROW_WALLET_EXCEPTION_IF(!r, error::tx_not_constructed, sd.sources, sd.splitted_dsts, sd.unlock_time, m_testnet);
// we don't test tx size, because we don't know the current limit, due to not having a blockchain,
// and it's a bit pointless to fail there anyway, since it'd be a (good) guess only. We sign anyway,
@@ -3978,6 +4056,7 @@ bool wallet2::sign_tx(unsigned_tx_set &exported_txs, const std::string &signed_f
ptx.selected_transfers = sd.selected_transfers;
ptx.tx_key = rct::rct2sk(rct::identity()); // don't send it back to the untrusted view wallet
ptx.dests = sd.dests;
+ ptx.msout = msout;
ptx.construction_data = sd;
txs.push_back(ptx);
@@ -3987,7 +4066,7 @@ bool wallet2::sign_tx(unsigned_tx_set &exported_txs, const std::string &signed_f
signed_txes.key_images.resize(m_transfers.size());
for (size_t i = 0; i < m_transfers.size(); ++i)
{
- if (!m_transfers[i].m_key_image_known)
+ if (!m_transfers[i].m_key_image_known || m_transfers[i].m_key_image_partial)
LOG_PRINT_L0("WARNING: key image not known in signing wallet at index " << i);
signed_txes.key_images[i] = m_transfers[i].m_key_image;
}
@@ -4113,11 +4192,12 @@ bool wallet2::load_tx(const std::string &signed_filename, std::vector<tools::wal
for (size_t i = 0; i < signed_txs.key_images.size(); ++i)
{
transfer_details &td = m_transfers[i];
- if (td.m_key_image_known && td.m_key_image != signed_txs.key_images[i])
+ if (td.m_key_image_known && !td.m_key_image_partial && td.m_key_image != signed_txs.key_images[i])
LOG_PRINT_L0("WARNING: imported key image differs from previously known key image at index " << i << ": trusting imported one");
td.m_key_image = signed_txs.key_images[i];
m_key_images[m_transfers[i].m_key_image] = i;
td.m_key_image_known = true;
+ td.m_key_image_partial = false;
m_pub_keys[m_transfers[i].get_public_key()] = i;
}
@@ -4126,6 +4206,217 @@ bool wallet2::load_tx(const std::string &signed_filename, std::vector<tools::wal
return true;
}
//----------------------------------------------------------------------------------------------------
+bool wallet2::save_multisig_tx(multisig_tx_set txs, const std::string &filename)
+{
+ LOG_PRINT_L0("saving " << txs.m_ptx.size() << " multisig transactions");
+
+ // txes generated, get rid of used k values
+ for (size_t n = 0; n < txs.m_ptx.size(); ++n)
+ for (size_t idx: txs.m_ptx[n].construction_data.selected_transfers)
+ m_transfers[idx].m_multisig_k = rct::zero();
+
+ // zero out some data we don't want to share
+ for (auto &ptx: txs.m_ptx)
+ {
+ for (auto &e: ptx.construction_data.sources)
+ e.multisig_kLRki.k = rct::zero();
+ }
+
+ for (auto &ptx: txs.m_ptx)
+ {
+ // Get decrypted payment id from pending_tx
+ ptx.construction_data = get_construction_data_with_decrypted_short_payment_id(ptx);
+ }
+
+ // save as binary
+ std::ostringstream oss;
+ boost::archive::portable_binary_oarchive ar(oss);
+ try
+ {
+ ar << txs;
+ }
+ catch (...)
+ {
+ return false;
+ }
+ LOG_PRINT_L2("Saving multisig unsigned tx data: " << oss.str());
+ std::string ciphertext = encrypt_with_view_secret_key(oss.str());
+ return epee::file_io_utils::save_string_to_file(filename, std::string(MULTISIG_UNSIGNED_TX_PREFIX) + ciphertext);
+}
+//----------------------------------------------------------------------------------------------------
+bool wallet2::save_multisig_tx(const std::vector<pending_tx>& ptx_vector, const std::string &filename)
+{
+ multisig_tx_set txs;
+ txs.m_ptx = ptx_vector;
+ crypto::hash hash;
+ cn_fast_hash(&get_account().get_keys().m_spend_secret_key, sizeof(crypto::secret_key), (char*)&hash);
+ txs.m_signers.insert(hash);
+ return save_multisig_tx(txs, filename);
+}
+//----------------------------------------------------------------------------------------------------
+bool wallet2::load_multisig_tx_from_file(const std::string &filename, multisig_tx_set &exported_txs, std::function<bool(const multisig_tx_set&)> accept_func)
+{
+ std::string s;
+ boost::system::error_code errcode;
+
+ if (!boost::filesystem::exists(filename, errcode))
+ {
+ LOG_PRINT_L0("File " << filename << " does not exist: " << errcode);
+ return false;
+ }
+ if (!epee::file_io_utils::load_file_to_string(filename.c_str(), s))
+ {
+ LOG_PRINT_L0("Failed to load from " << filename);
+ return false;
+ }
+ const size_t magiclen = strlen(MULTISIG_UNSIGNED_TX_PREFIX);
+ if (strncmp(s.c_str(), MULTISIG_UNSIGNED_TX_PREFIX, magiclen))
+ {
+ LOG_PRINT_L0("Bad magic from " << filename);
+ return false;
+ }
+ try
+ {
+ s = decrypt_with_view_secret_key(std::string(s, magiclen));
+ }
+ catch (const std::exception &e)
+ {
+ LOG_PRINT_L0("Failed to decrypt " << filename << ": " << e.what());
+ return 0;
+ }
+ try
+ {
+ std::istringstream iss(s);
+ boost::archive::portable_binary_iarchive ar(iss);
+ ar >> exported_txs;
+ }
+ catch (...)
+ {
+ LOG_PRINT_L0("Failed to parse data from " << filename);
+ return false;
+ }
+
+ // sanity checks
+ for (const auto &ptx: exported_txs.m_ptx)
+ {
+ CHECK_AND_ASSERT_MES(ptx.selected_transfers.size() == ptx.tx.vin.size(), false, "Mismatched selected_transfers/vin sizes");
+ for (size_t idx: ptx.selected_transfers)
+ CHECK_AND_ASSERT_MES(idx < m_transfers.size(), false, "Transfer index out of range");
+ CHECK_AND_ASSERT_MES(ptx.construction_data.selected_transfers.size() == ptx.tx.vin.size(), false, "Mismatched cd selected_transfers/vin sizes");
+ for (size_t idx: ptx.construction_data.selected_transfers)
+ CHECK_AND_ASSERT_MES(idx < m_transfers.size(), false, "Transfer index out of range");
+ CHECK_AND_ASSERT_MES(ptx.construction_data.sources.size() == ptx.tx.vin.size(), false, "Mismatched sources/vin sizes");
+ }
+
+ LOG_PRINT_L1("Loaded multisig tx unsigned data from binary: " << exported_txs.m_ptx.size() << " transactions");
+ for (auto &ptx: exported_txs.m_ptx) LOG_PRINT_L0(cryptonote::obj_to_json_str(ptx.tx));
+
+ if (accept_func && !accept_func(exported_txs))
+ {
+ LOG_PRINT_L1("Transactions rejected by callback");
+ return false;
+ }
+
+ const bool is_signed = exported_txs.m_signers.size() >= m_multisig_threshold;
+ if (is_signed)
+ {
+ for (const auto &ptx: exported_txs.m_ptx)
+ {
+ const crypto::hash txid = get_transaction_hash(ptx.tx);
+ if (store_tx_info())
+ {
+ m_tx_keys.insert(std::make_pair(txid, ptx.tx_key));
+ m_additional_tx_keys.insert(std::make_pair(txid, ptx.additional_tx_keys));
+ }
+ }
+ }
+
+ return true;
+}
+//----------------------------------------------------------------------------------------------------
+bool wallet2::sign_multisig_tx(multisig_tx_set &exported_txs, const std::string &filename, std::vector<crypto::hash> &txids)
+{
+ THROW_WALLET_EXCEPTION_IF(exported_txs.m_ptx.empty(), error::wallet_internal_error, "No tx found");
+
+ txids.clear();
+
+ // sign the transactions
+ for (size_t n = 0; n < exported_txs.m_ptx.size(); ++n)
+ {
+ tools::wallet2::pending_tx &ptx = exported_txs.m_ptx[n];
+ tools::wallet2::tx_construction_data &sd = ptx.construction_data;
+ LOG_PRINT_L1(" " << (n+1) << ": " << sd.sources.size() << " inputs, mixin " << (sd.sources[0].outputs.size()-1) <<
+ ", signed by " << exported_txs.m_signers.size() << "/" << m_multisig_threshold);
+ cryptonote::transaction tx;
+ rct::multisig_out msout = ptx.msout;
+ auto sources = sd.sources;
+ const bool bulletproof = sd.use_rct && (ptx.tx.rct_signatures.type == rct::RCTTypeFullBulletproof || ptx.tx.rct_signatures.type == rct::RCTTypeSimpleBulletproof);
+ bool r = cryptonote::construct_tx_with_tx_key(m_account.get_keys(), m_subaddresses, sources, sd.splitted_dsts, ptx.change_dts.addr, sd.extra, tx, sd.unlock_time, ptx.tx_key, ptx.additional_tx_keys, sd.use_rct, bulletproof, &msout);
+ THROW_WALLET_EXCEPTION_IF(!r, error::tx_not_constructed, sd.sources, sd.splitted_dsts, sd.unlock_time, m_testnet);
+
+ THROW_WALLET_EXCEPTION_IF(get_transaction_prefix_hash (tx) != get_transaction_prefix_hash(ptx.tx),
+ error::wallet_internal_error, "Transaction prefix does not match data");
+
+ // Tests passed, sign
+ std::vector<unsigned int> indices;
+ for (const auto &source: sources)
+ indices.push_back(source.real_output);
+
+ rct::keyV k;
+ for (size_t idx: sd.selected_transfers)
+ k.push_back(get_multisig_k(idx));
+
+ THROW_WALLET_EXCEPTION_IF(!rct::signMultisig(ptx.tx.rct_signatures, indices, k, ptx.msout, rct::sk2rct(get_account().get_keys().m_spend_secret_key)),
+ error::wallet_internal_error, "Failed signing, transaction likely malformed");
+
+ const bool is_last = exported_txs.m_signers.size() + 1 >= m_multisig_threshold;
+ if (is_last)
+ {
+ const crypto::hash txid = get_transaction_hash(ptx.tx);
+ if (store_tx_info())
+ {
+ m_tx_keys.insert(std::make_pair(txid, ptx.tx_key));
+ m_additional_tx_keys.insert(std::make_pair(txid, ptx.additional_tx_keys));
+ }
+ txids.push_back(txid);
+ }
+ }
+
+ // txes generated, get rid of used k values
+ for (size_t n = 0; n < exported_txs.m_ptx.size(); ++n)
+ for (size_t idx: exported_txs.m_ptx[n].construction_data.selected_transfers)
+ m_transfers[idx].m_multisig_k = rct::zero();
+
+ crypto::hash hash;
+ cn_fast_hash(&get_account().get_keys().m_spend_secret_key, sizeof(crypto::secret_key), (char*)&hash);
+ exported_txs.m_signers.insert(hash);
+
+ return save_multisig_tx(exported_txs, filename);
+}
+//----------------------------------------------------------------------------------------------------
+bool wallet2::sign_multisig_tx_from_file(const std::string &filename, std::vector<crypto::hash> &txids, std::function<bool(const multisig_tx_set&)> accept_func)
+{
+ multisig_tx_set exported_txs;
+ if(!load_multisig_tx_from_file(filename, exported_txs))
+ return false;
+
+ crypto::hash hash;
+ cn_fast_hash(&get_account().get_keys().m_spend_secret_key, sizeof(crypto::secret_key), (char*)&hash);
+ THROW_WALLET_EXCEPTION_IF(exported_txs.m_signers.find(hash) != exported_txs.m_signers.end(),
+ error::wallet_internal_error, "Transaction already signed by this private key");
+ THROW_WALLET_EXCEPTION_IF(exported_txs.m_signers.size() > m_multisig_threshold,
+ error::wallet_internal_error, "Transaction was signed by too many signers");
+ THROW_WALLET_EXCEPTION_IF(exported_txs.m_signers.size() == m_multisig_threshold,
+ error::wallet_internal_error, "Transaction is already fully signed");
+
+ if (accept_func && !accept_func(exported_txs))
+ {
+ LOG_PRINT_L1("Transactions rejected by callback");
+ return false;
+ }
+ return sign_multisig_tx(exported_txs, filename, txids);
+}
+//----------------------------------------------------------------------------------------------------
uint64_t wallet2::get_fee_multiplier(uint32_t priority, int fee_algorithm)
{
static const uint64_t old_multipliers[3] = {1, 2, 3};
@@ -4749,6 +5040,10 @@ void wallet2::transfer_selected(const std::vector<cryptonote::tx_destination_ent
src.real_out_additional_tx_keys = get_additional_tx_pub_keys_from_extra(td.m_tx);
src.real_output = it_to_replace - src.outputs.begin();
src.real_output_in_tx_index = td.m_internal_output_index;
+ if (m_multisig)
+ src.multisig_kLRki = get_multisig_composite_LRki(idx, get_multisig_k(idx));
+ else
+ src.multisig_kLRki = rct::multisig_kLRki({rct::zero(), rct::zero(), rct::zero(), rct::zero()});
detail::print_source_entry(src);
++out_index;
}
@@ -4776,8 +5071,9 @@ void wallet2::transfer_selected(const std::vector<cryptonote::tx_destination_ent
crypto::secret_key tx_key;
std::vector<crypto::secret_key> additional_tx_keys;
+ rct::multisig_out msout;
LOG_PRINT_L2("constructing tx");
- bool r = cryptonote::construct_tx_and_get_tx_key(m_account.get_keys(), m_subaddresses, sources, splitted_dsts, change_dts.addr, extra, tx, unlock_time, tx_key, additional_tx_keys);
+ bool r = cryptonote::construct_tx_and_get_tx_key(m_account.get_keys(), m_subaddresses, sources, splitted_dsts, change_dts.addr, extra, tx, unlock_time, tx_key, additional_tx_keys, false, false, m_multisig ? &msout : NULL);
LOG_PRINT_L2("constructed tx, r="<<r);
THROW_WALLET_EXCEPTION_IF(!r, error::tx_not_constructed, sources, splitted_dsts, unlock_time, m_testnet);
THROW_WALLET_EXCEPTION_IF(upper_transaction_size_limit <= get_object_blobsize(tx), error::tx_too_big, tx, upper_transaction_size_limit);
@@ -4807,6 +5103,7 @@ void wallet2::transfer_selected(const std::vector<cryptonote::tx_destination_ent
ptx.tx_key = tx_key;
ptx.additional_tx_keys = additional_tx_keys;
ptx.dests = dsts;
+ ptx.msout = msout;
ptx.construction_data.sources = sources;
ptx.construction_data.change_dts = change_dts;
ptx.construction_data.splitted_dsts = splitted_dsts;
@@ -4907,6 +5204,10 @@ void wallet2::transfer_selected_rct(std::vector<cryptonote::tx_destination_entry
src.real_output = it_to_replace - src.outputs.begin();
src.real_output_in_tx_index = td.m_internal_output_index;
src.mask = td.m_mask;
+ if (m_multisig)
+ src.multisig_kLRki = get_multisig_composite_LRki(idx, get_multisig_k(idx));
+ else
+ src.multisig_kLRki = rct::multisig_kLRki({rct::zero(), rct::zero(), rct::zero(), rct::zero()});
detail::print_source_entry(src);
++out_index;
}
@@ -4940,12 +5241,28 @@ void wallet2::transfer_selected_rct(std::vector<cryptonote::tx_destination_entry
crypto::secret_key tx_key;
std::vector<crypto::secret_key> additional_tx_keys;
+ rct::multisig_out msout;
LOG_PRINT_L2("constructing tx");
- bool r = cryptonote::construct_tx_and_get_tx_key(m_account.get_keys(), m_subaddresses, sources, splitted_dsts, change_dts.addr, extra, tx, unlock_time, tx_key, additional_tx_keys, true, bulletproof);
+ auto sources_copy = sources;
+ bool r = cryptonote::construct_tx_and_get_tx_key(m_account.get_keys(), m_subaddresses, sources, splitted_dsts, change_dts.addr, extra, tx, unlock_time, tx_key, additional_tx_keys, true, bulletproof, m_multisig ? &msout : NULL);
LOG_PRINT_L2("constructed tx, r="<<r);
THROW_WALLET_EXCEPTION_IF(!r, error::tx_not_constructed, sources, dsts, unlock_time, m_testnet);
THROW_WALLET_EXCEPTION_IF(upper_transaction_size_limit <= get_object_blobsize(tx), error::tx_too_big, tx, upper_transaction_size_limit);
+ // work out the permutation done on sources
+ std::vector<size_t> ins_order;
+ for (size_t n = 0; n < sources.size(); ++n)
+ {
+ for (size_t idx = 0; idx < sources_copy.size(); ++idx)
+ {
+ THROW_WALLET_EXCEPTION_IF((size_t)sources_copy[idx].real_output >= sources_copy[idx].outputs.size(),
+ error::wallet_internal_error, "Invalid real_output");
+ if (sources_copy[idx].outputs[sources_copy[idx].real_output].second.dest == sources[n].outputs[sources[n].real_output].second.dest)
+ ins_order.push_back(idx);
+ }
+ }
+ THROW_WALLET_EXCEPTION_IF(ins_order.size() != sources.size(), error::wallet_internal_error, "Failed to work out sources permutation");
+
LOG_PRINT_L2("gathering key images");
std::string key_images;
bool all_are_txin_to_key = std::all_of(tx.vin.begin(), tx.vin.end(), [&](const txin_v& s_e) -> bool
@@ -4964,13 +5281,15 @@ void wallet2::transfer_selected_rct(std::vector<cryptonote::tx_destination_entry
ptx.tx = tx;
ptx.change_dts = change_dts;
ptx.selected_transfers = selected_transfers;
+ tools::apply_permutation(ins_order, ptx.selected_transfers);
ptx.tx_key = tx_key;
ptx.additional_tx_keys = additional_tx_keys;
ptx.dests = dsts;
+ ptx.msout = msout;
ptx.construction_data.sources = sources;
ptx.construction_data.change_dts = change_dts;
ptx.construction_data.splitted_dsts = splitted_dsts;
- ptx.construction_data.selected_transfers = selected_transfers;
+ ptx.construction_data.selected_transfers = ptx.selected_transfers;
ptx.construction_data.extra = tx.extra;
ptx.construction_data.unlock_time = unlock_time;
ptx.construction_data.use_rct = true;
@@ -4997,13 +5316,13 @@ std::vector<size_t> wallet2::pick_preferred_rct_inputs(uint64_t needed_money, ui
for (size_t i = 0; i < m_transfers.size(); ++i)
{
const transfer_details& td = m_transfers[i];
- if (!td.m_spent && td.is_rct() && is_transfer_unlocked(td) && td.m_subaddr_index.major == subaddr_account && subaddr_indices.count(td.m_subaddr_index.minor) == 1)
+ if (!td.m_spent && !td.m_key_image_partial && td.is_rct() && is_transfer_unlocked(td) && td.m_subaddr_index.major == subaddr_account && subaddr_indices.count(td.m_subaddr_index.minor) == 1)
{
LOG_PRINT_L2("Considering input " << i << ", " << print_money(td.amount()));
for (size_t j = i + 1; j < m_transfers.size(); ++j)
{
const transfer_details& td2 = m_transfers[j];
- if (!td2.m_spent && td2.is_rct() && td.amount() + td2.amount() >= needed_money && is_transfer_unlocked(td2) && td2.m_subaddr_index == td.m_subaddr_index)
+ if (!td2.m_spent && !td.m_key_image_partial && td2.is_rct() && td.amount() + td2.amount() >= needed_money && is_transfer_unlocked(td2) && td2.m_subaddr_index == td.m_subaddr_index)
{
// update our picks if those outputs are less related than any we
// already found. If the same, don't update, and oldest suitable outputs
@@ -5665,7 +5984,7 @@ std::vector<wallet2::pending_tx> wallet2::create_transactions_2(std::vector<cryp
for (size_t i = 0; i < m_transfers.size(); ++i)
{
const transfer_details& td = m_transfers[i];
- if (!td.m_spent && (use_rct ? true : !td.is_rct()) && is_transfer_unlocked(td) && td.m_subaddr_index.major == subaddr_account && subaddr_indices.count(td.m_subaddr_index.minor) == 1)
+ if (!td.m_spent && !td.m_key_image_partial && (use_rct ? true : !td.is_rct()) && is_transfer_unlocked(td) && td.m_subaddr_index.major == subaddr_account && subaddr_indices.count(td.m_subaddr_index.minor) == 1)
{
const uint32_t index_minor = td.m_subaddr_index.minor;
auto find_predicate = [&index_minor](const std::pair<uint32_t, std::vector<size_t>>& x) { return x.first == index_minor; };
@@ -6041,7 +6360,7 @@ std::vector<wallet2::pending_tx> wallet2::create_transactions_all(uint64_t below
for (size_t i = 0; i < m_transfers.size(); ++i)
{
const transfer_details& td = m_transfers[i];
- if (!td.m_spent && (use_rct ? true : !td.is_rct()) && is_transfer_unlocked(td) && td.m_subaddr_index.major == subaddr_account && subaddr_indices.count(td.m_subaddr_index.minor) == 1)
+ if (!td.m_spent && !td.m_key_image_partial && (use_rct ? true : !td.is_rct()) && is_transfer_unlocked(td) && td.m_subaddr_index.major == subaddr_account && subaddr_indices.count(td.m_subaddr_index.minor) == 1)
{
if (below == 0 || td.amount() < below)
{
@@ -6255,6 +6574,8 @@ std::vector<size_t> wallet2::select_available_outputs(const std::function<bool(c
{
if (i->m_spent)
continue;
+ if (i->m_key_image_partial)
+ continue;
if (!is_transfer_unlocked(*i))
continue;
if (f(*i))
@@ -7235,7 +7556,7 @@ std::vector<std::pair<crypto::key_image, crypto::signature>> wallet2::export_key
bool r = cryptonote::generate_key_image_helper(m_account.get_keys(), m_subaddresses, pkey, tx_pub_key, additional_tx_pub_keys, td.m_internal_output_index, in_ephemeral, ki);
THROW_WALLET_EXCEPTION_IF(!r, error::wallet_internal_error, "Failed to generate key image");
- THROW_WALLET_EXCEPTION_IF(td.m_key_image_known && ki != td.m_key_image,
+ THROW_WALLET_EXCEPTION_IF(td.m_key_image_known && !td.m_key_image_partial && ki != td.m_key_image,
error::wallet_internal_error, "key_image generated not matched with cached key image");
THROW_WALLET_EXCEPTION_IF(in_ephemeral.pub != pkey,
error::wallet_internal_error, "key_image generated ephemeral public key not matched with output_key");
@@ -7350,6 +7671,7 @@ uint64_t wallet2::import_key_images(const std::vector<std::pair<crypto::key_imag
m_transfers[n].m_key_image = signed_key_images[n].first;
m_key_images[m_transfers[n].m_key_image] = n;
m_transfers[n].m_key_image_known = true;
+ m_transfers[n].m_key_image_partial = false;
}
if(check_spent)
@@ -7634,6 +7956,7 @@ size_t wallet2::import_outputs(const std::vector<tools::wallet2::transfer_detail
THROW_WALLET_EXCEPTION_IF(!r, error::wallet_internal_error, "Failed to generate key image");
expand_subaddresses(td.m_subaddr_index);
td.m_key_image_known = true;
+ td.m_key_image_partial = false;
THROW_WALLET_EXCEPTION_IF(in_ephemeral.pub != boost::get<cryptonote::txout_to_key>(td.m_tx.vout[td.m_internal_output_index].target).key,
error::wallet_internal_error, "key_image generated ephemeral public key not matched with output_key at index " + boost::lexical_cast<std::string>(i));
@@ -7645,6 +7968,138 @@ size_t wallet2::import_outputs(const std::vector<tools::wallet2::transfer_detail
return m_transfers.size();
}
//----------------------------------------------------------------------------------------------------
+rct::key wallet2::get_multisig_k(size_t idx) const
+{
+ CHECK_AND_ASSERT_THROW_MES(m_multisig, "Wallet is not multisig");
+ CHECK_AND_ASSERT_THROW_MES(idx < m_transfers.size(), "Index out of range");
+ THROW_WALLET_EXCEPTION_IF(m_transfers[idx].m_multisig_k == rct::zero(), error::multisig_export_needed);
+ return m_transfers[idx].m_multisig_k;
+}
+//----------------------------------------------------------------------------------------------------
+rct::multisig_kLRki wallet2::get_multisig_LRki(size_t n, const rct::key &k) const
+{
+ rct::multisig_kLRki kLRki;
+ kLRki.k = k;
+ rct::scalarmultBase(kLRki.L, kLRki.k);
+ crypto::generate_key_image(m_transfers[n].get_public_key(), rct::rct2sk(kLRki.k), (crypto::key_image&)kLRki.R);
+ kLRki.ki = rct::ki2rct(m_transfers[n].m_key_image);
+ return kLRki;
+}
+//----------------------------------------------------------------------------------------------------
+rct::multisig_kLRki wallet2::get_multisig_composite_LRki(size_t n, const rct::key &k) const
+{
+ rct::multisig_kLRki kLRki = get_multisig_LRki(n, k);
+ const transfer_details &td = m_transfers[n];
+ crypto::public_key tx_key = get_tx_pub_key_from_received_outs(td);
+ cryptonote::keypair in_ephemeral;
+ bool r = wallet_generate_key_image_helper_old(get_account().get_keys(), tx_key, td.m_internal_output_index, in_ephemeral, (crypto::key_image&)kLRki.ki);
+ CHECK_AND_ASSERT_THROW_MES(r, "Failed to generate key image");
+ for (const auto &info: td.m_multisig_info)
+ {
+ rct::addKeys(kLRki.ki, kLRki.ki, rct::ki2rct(info.m_partial_key_image));
+ rct::addKeys(kLRki.L, kLRki.L, info.m_L);
+ rct::addKeys(kLRki.R, kLRki.R, info.m_R);
+ }
+ return kLRki;
+}
+//----------------------------------------------------------------------------------------------------
+std::vector<tools::wallet2::multisig_info> wallet2::export_multisig()
+{
+ std::vector<tools::wallet2::multisig_info> info;
+
+ info.resize(m_transfers.size());
+ for (size_t n = 0; n < m_transfers.size(); ++n)
+ {
+ transfer_details &td = m_transfers[n];
+ crypto::public_key tx_key = get_tx_pub_key_from_received_outs(td);
+ cryptonote::keypair in_ephemeral;
+ crypto::key_image ki;
+ td.m_multisig_k = rct::skGen();
+ const rct::multisig_kLRki kLRki = get_multisig_LRki(n, td.m_multisig_k);
+ const std::vector<crypto::public_key> additional_tx_pub_keys = get_additional_tx_pub_keys_from_extra(td.m_tx);
+ // we want to export the partial key image, not the full one, so we can't use td.m_key_image
+ bool r = wallet_generate_key_image_helper_old(get_account().get_keys(), tx_key, td.m_internal_output_index, in_ephemeral, ki, true);
+ CHECK_AND_ASSERT_THROW_MES(r, "Failed to generate key image");
+ // we got the ephemeral keypair, but the key image isn't right as it's done as per our private spend key, which is multisig
+ crypto::generate_key_image(in_ephemeral.pub, get_account().get_keys().m_spend_secret_key, ki);
+ info[n] = multisig_info({ki, kLRki.L, kLRki.R});
+ }
+
+ return info;
+}
+//----------------------------------------------------------------------------------------------------
+void wallet2::update_multisig_rescan_info(const std::vector<rct::key> &multisig_k, const std::vector<std::vector<tools::wallet2::multisig_info>> &info, size_t n)
+{
+ CHECK_AND_ASSERT_THROW_MES(n < m_transfers.size(), "Bad index in update_multisig_info");
+ CHECK_AND_ASSERT_THROW_MES(multisig_k.size() >= m_transfers.size(), "Mismatched sizes of multisig_k and info");
+
+ MDEBUG("update_multisig_rescan_info: updating index " << n);
+ transfer_details &td = m_transfers[n];
+ td.m_multisig_info.clear();
+ for (const auto &pi: info)
+ {
+ CHECK_AND_ASSERT_THROW_MES(n < pi.size(), "Bad pi size");
+ td.m_multisig_info.push_back(pi[n]);
+ }
+ m_key_images.erase(td.m_key_image);
+ td.m_key_image = rct::rct2ki(get_multisig_composite_LRki(n, rct::skGen()).ki);
+ td.m_key_image_known = true;
+ td.m_key_image_partial = false;
+ td.m_multisig_k = multisig_k[n];
+ m_key_images[td.m_key_image] = n;
+}
+//----------------------------------------------------------------------------------------------------
+size_t wallet2::import_multisig(std::vector<std::vector<tools::wallet2::multisig_info>> info)
+{
+ CHECK_AND_ASSERT_THROW_MES(m_multisig, "Wallet is not multisig");
+ CHECK_AND_ASSERT_THROW_MES(info.size() + 1 == m_multisig_total, "Wrong number of multisig sources");
+
+ std::vector<rct::key> k;
+ k.reserve(m_transfers.size());
+ for (const auto &td: m_transfers)
+ k.push_back(td.m_multisig_k);
+
+ // how many outputs we're going to update
+ size_t n_outputs = m_transfers.size();
+ for (const auto &pi: info)
+ if (pi.size() < n_outputs)
+ n_outputs = pi.size();
+
+ // trim data we don't have info for from all participants
+ for (auto &pi: info)
+ pi.resize(n_outputs);
+
+ // first pass to determine where to detach the blockchain
+ for (size_t n = 0; n < n_outputs; ++n)
+ {
+ const transfer_details &td = m_transfers[n];
+ if (!td.m_key_image_partial)
+ continue;
+ MINFO("Multisig info importing from block height " << td.m_block_height);
+ detach_blockchain(td.m_block_height);
+ break;
+ }
+
+ MFATAL("import_multisig: updating from import");
+ for (size_t n = 0; n < n_outputs && n < m_transfers.size(); ++n)
+ {
+ update_multisig_rescan_info(k, info, n);
+ }
+
+ m_multisig_rescan_k = &k;
+ m_multisig_rescan_info = &info;
+ try
+ {
+ MFATAL("import_multisig: refreshing");
+ refresh();
+ }
+ catch (...) {}
+ m_multisig_rescan_info = NULL;
+ m_multisig_rescan_k = NULL;
+
+ return n_outputs;
+}
+//----------------------------------------------------------------------------------------------------
std::string wallet2::encrypt(const std::string &plaintext, const crypto::secret_key &skey, bool authenticated) const
{
crypto::chacha8_key key;