aboutsummaryrefslogtreecommitdiff
path: root/src/wallet
diff options
context:
space:
mode:
Diffstat (limited to '')
-rw-r--r--src/wallet/api/wallet2_api.h4
-rw-r--r--src/wallet/api/wallet_manager.cpp4
-rw-r--r--src/wallet/api/wallet_manager.h2
-rw-r--r--src/wallet/wallet2.cpp575
-rw-r--r--src/wallet/wallet2.h100
-rw-r--r--src/wallet/wallet_errors.h19
-rw-r--r--src/wallet/wallet_rpc_server.cpp1
-rw-r--r--src/wallet/wallet_rpc_server_commands_defs.h2
8 files changed, 634 insertions, 73 deletions
diff --git a/src/wallet/api/wallet2_api.h b/src/wallet/api/wallet2_api.h
index b1f8369a3..8593bd1f9 100644
--- a/src/wallet/api/wallet2_api.h
+++ b/src/wallet/api/wallet2_api.h
@@ -812,10 +812,10 @@ struct WalletManager
* @brief verifyWalletPassword - check if the given filename is the wallet
* @param keys_file_name - location of keys file
* @param password - password to verify
- * @param watch_only - verify only view keys?
+ * @param no_spend_key - verify only view keys?
* @return - true if password is correct
*/
- virtual bool verifyWalletPassword(const std::string &keys_file_name, const std::string &password, bool watch_only) const = 0;
+ virtual bool verifyWalletPassword(const std::string &keys_file_name, const std::string &password, bool no_spend_key) const = 0;
/*!
* \brief findWallets - searches for the wallet files by given path name recursively
diff --git a/src/wallet/api/wallet_manager.cpp b/src/wallet/api/wallet_manager.cpp
index ee69ec028..a6e5e551e 100644
--- a/src/wallet/api/wallet_manager.cpp
+++ b/src/wallet/api/wallet_manager.cpp
@@ -127,9 +127,9 @@ bool WalletManagerImpl::walletExists(const std::string &path)
return false;
}
-bool WalletManagerImpl::verifyWalletPassword(const std::string &keys_file_name, const std::string &password, bool watch_only) const
+bool WalletManagerImpl::verifyWalletPassword(const std::string &keys_file_name, const std::string &password, bool no_spend_key) const
{
- return tools::wallet2::verify_password(keys_file_name, password, watch_only);
+ return tools::wallet2::verify_password(keys_file_name, password, no_spend_key);
}
std::vector<std::string> WalletManagerImpl::findWallets(const std::string &path)
diff --git a/src/wallet/api/wallet_manager.h b/src/wallet/api/wallet_manager.h
index 978a2d411..ef5b8f91b 100644
--- a/src/wallet/api/wallet_manager.h
+++ b/src/wallet/api/wallet_manager.h
@@ -50,7 +50,7 @@ public:
const std::string &spendKeyString = "");
virtual bool closeWallet(Wallet *wallet, bool store = true);
bool walletExists(const std::string &path);
- bool verifyWalletPassword(const std::string &keys_file_name, const std::string &password, bool watch_only) const;
+ bool verifyWalletPassword(const std::string &keys_file_name, const std::string &password, bool no_spend_key) const;
std::vector<std::string> findWallets(const std::string &path);
std::string errorString() const;
void setDaemonAddress(const std::string &address);
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;
diff --git a/src/wallet/wallet2.h b/src/wallet/wallet2.h
index 9b068df3c..97abb1476 100644
--- a/src/wallet/wallet2.h
+++ b/src/wallet/wallet2.h
@@ -145,7 +145,7 @@ namespace tools
};
private:
- wallet2(const wallet2&) : m_run(true), m_callback(0), m_testnet(false), m_always_confirm_transfers(true), m_print_ring_members(false), m_store_tx_info(true), m_default_mixin(0), m_default_priority(0), m_refresh_type(RefreshDefault), m_auto_refresh(true), m_refresh_from_block_height(0), m_confirm_missing_payment_id(true), m_ask_password(true), m_min_output_count(0), m_min_output_value(0), m_merge_destinations(false), m_confirm_backlog(true), m_is_initialized(false),m_node_rpc_proxy(m_http_client, m_daemon_rpc_mutex) {}
+ wallet2(const wallet2&) : m_multisig_rescan_info(NULL), m_multisig_rescan_k(NULL), m_run(true), m_callback(0), m_testnet(false), m_always_confirm_transfers(true), m_print_ring_members(false), m_store_tx_info(true), m_default_mixin(0), m_default_priority(0), m_refresh_type(RefreshDefault), m_auto_refresh(true), m_refresh_from_block_height(0), m_confirm_missing_payment_id(true), m_ask_password(true), m_min_output_count(0), m_min_output_value(0), m_merge_destinations(false), m_confirm_backlog(true), m_is_initialized(false),m_node_rpc_proxy(m_http_client, m_daemon_rpc_mutex) {}
public:
static const char* tr(const char* str);
@@ -168,7 +168,20 @@ namespace tools
static bool verify_password(const std::string& keys_file_name, const epee::wipeable_string& password, bool no_spend_key);
- wallet2(bool testnet = false, bool restricted = false) : m_run(true), m_callback(0), m_testnet(testnet), m_always_confirm_transfers(true), m_print_ring_members(false), m_store_tx_info(true), m_default_mixin(0), m_default_priority(0), m_refresh_type(RefreshDefault), m_auto_refresh(true), m_refresh_from_block_height(0), m_confirm_missing_payment_id(true), m_ask_password(true), m_min_output_count(0), m_min_output_value(0), m_merge_destinations(false), m_confirm_backlog(true), m_is_initialized(false), m_restricted(restricted), is_old_file_format(false), m_node_rpc_proxy(m_http_client, m_daemon_rpc_mutex), m_light_wallet(false), m_light_wallet_scanned_block_height(0), m_light_wallet_blockchain_height(0), m_light_wallet_connected(false), m_light_wallet_balance(0), m_light_wallet_unlocked_balance(0) {}
+ wallet2(bool testnet = false, bool restricted = false) : m_multisig_rescan_info(NULL), m_multisig_rescan_k(NULL), m_run(true), m_callback(0), m_testnet(testnet), m_always_confirm_transfers(true), m_print_ring_members(false), m_store_tx_info(true), m_default_mixin(0), m_default_priority(0), m_refresh_type(RefreshDefault), m_auto_refresh(true), m_refresh_from_block_height(0), m_confirm_missing_payment_id(true), m_ask_password(true), m_min_output_count(0), m_min_output_value(0), m_merge_destinations(false), m_confirm_backlog(true), m_is_initialized(false), m_restricted(restricted), is_old_file_format(false), m_node_rpc_proxy(m_http_client, m_daemon_rpc_mutex), m_light_wallet(false), m_light_wallet_scanned_block_height(0), m_light_wallet_blockchain_height(0), m_light_wallet_connected(false), m_light_wallet_balance(0), m_light_wallet_unlocked_balance(0) {}
+
+ struct multisig_info
+ {
+ crypto::key_image m_partial_key_image;
+ rct::key m_L;
+ rct::key m_R;
+
+ BEGIN_SERIALIZE_OBJECT()
+ FIELD(m_partial_key_image)
+ FIELD(m_L)
+ FIELD(m_R)
+ END_SERIALIZE()
+ };
struct tx_scan_info_t
{
@@ -199,6 +212,9 @@ namespace tools
bool m_key_image_known;
size_t m_pk_index;
cryptonote::subaddress_index m_subaddr_index;
+ bool m_key_image_partial;
+ rct::key m_multisig_k;
+ std::vector<multisig_info> m_multisig_info;
bool is_rct() const { return m_rct; }
uint64_t amount() const { return m_amount; }
@@ -219,6 +235,9 @@ namespace tools
FIELD(m_key_image_known)
FIELD(m_pk_index)
FIELD(m_subaddr_index)
+ FIELD(m_key_image_partial)
+ FIELD(m_multisig_k)
+ FIELD(m_multisig_info)
END_SERIALIZE()
};
@@ -322,6 +341,7 @@ namespace tools
crypto::secret_key tx_key;
std::vector<crypto::secret_key> additional_tx_keys;
std::vector<cryptonote::tx_destination_entry> dests;
+ rct::multisig_out msout;
tx_construction_data construction_data;
@@ -354,6 +374,12 @@ namespace tools
std::vector<crypto::key_image> key_images;
};
+ struct multisig_tx_set
+ {
+ std::vector<pending_tx> m_ptx;
+ std::unordered_set<crypto::hash> m_signers;
+ };
+
struct keys_file_data
{
crypto::chacha8_iv iv;
@@ -434,6 +460,16 @@ namespace tools
*/
static bool verify_multisig_info(const std::string &data, crypto::secret_key &skey, crypto::public_key &pkey);
/*!
+ * Export multisig info
+ * This will generate and remember new k values
+ */
+ std::vector<tools::wallet2::multisig_info> export_multisig();
+ /*!
+ * Import a set of multisig info from multisig partners
+ * \return the number of inputs which were imported
+ */
+ size_t import_multisig(std::vector<std::vector<tools::wallet2::multisig_info>> info);
+ /*!
* \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)
* \param password Password for wallet file
@@ -528,6 +564,7 @@ namespace tools
bool restricted() const { return m_restricted; }
bool watch_only() const { return m_watch_only; }
bool multisig(uint32_t *threshold = NULL, uint32_t *total = NULL) const;
+ bool has_multisig_partial_key_images() const;
// locked & unlocked balance of given or current subaddress account
uint64_t balance(uint32_t subaddr_index_major) const;
@@ -555,6 +592,8 @@ namespace tools
void commit_tx(pending_tx& ptx_vector);
void commit_tx(std::vector<pending_tx>& ptx_vector);
bool save_tx(const std::vector<pending_tx>& ptx_vector, const std::string &filename);
+ bool save_multisig_tx(multisig_tx_set txs, const std::string &filename);
+ bool save_multisig_tx(const std::vector<pending_tx>& ptx_vector, const std::string &filename);
// load unsigned tx from file and sign it. Takes confirmation callback as argument. Used by the cli wallet
bool sign_tx(const std::string &unsigned_filename, const std::string &signed_filename, std::vector<wallet2::pending_tx> &ptx, std::function<bool(const unsigned_tx_set&)> accept_func = NULL, bool export_raw = false);
// sign unsigned tx. Takes unsigned_tx_set as argument. Used by GUI
@@ -567,6 +606,9 @@ namespace tools
std::vector<wallet2::pending_tx> create_transactions_all(uint64_t below, const cryptonote::account_public_address &address, bool is_subaddress, const size_t fake_outs_count, const uint64_t unlock_time, uint32_t priority, const std::vector<uint8_t>& extra, uint32_t subaddr_account, std::set<uint32_t> subaddr_indices, bool trusted_daemon);
std::vector<wallet2::pending_tx> create_transactions_single(const crypto::key_image &ki, const cryptonote::account_public_address &address, bool is_subaddress, const size_t fake_outs_count, const uint64_t unlock_time, uint32_t priority, const std::vector<uint8_t>& extra, bool trusted_daemon);
std::vector<wallet2::pending_tx> create_transactions_from(const cryptonote::account_public_address &address, bool is_subaddress, std::vector<size_t> unused_transfers_indices, std::vector<size_t> unused_dust_indices, const size_t fake_outs_count, const uint64_t unlock_time, uint32_t priority, const std::vector<uint8_t>& extra, bool trusted_daemon);
+ bool load_multisig_tx_from_file(const std::string &filename, multisig_tx_set &exported_txs, std::function<bool(const multisig_tx_set&)> accept_func = NULL);
+ bool sign_multisig_tx_from_file(const std::string &filename, std::vector<crypto::hash> &txids, std::function<bool(const multisig_tx_set&)> accept_func);
+ bool sign_multisig_tx(multisig_tx_set &exported_txs, const std::string &filename, std::vector<crypto::hash> &txids);
std::vector<pending_tx> create_unmixable_sweep_transactions(bool trusted_daemon);
bool check_connection(uint32_t *version = NULL, uint32_t timeout = 200000);
void get_transfers(wallet2::transfer_container& incoming_transfers) const;
@@ -881,7 +923,6 @@ namespace tools
void check_genesis(const crypto::hash& genesis_hash) const; //throws
bool generate_chacha8_key_from_secret_keys(crypto::chacha8_key &key) const;
crypto::hash get_payment_id(const pending_tx &ptx) const;
- crypto::hash8 get_short_payment_id(const pending_tx &ptx) const;
void check_acc_out_precomp(const cryptonote::tx_out &o, const crypto::key_derivation &derivation, const std::vector<crypto::key_derivation> &additional_derivations, size_t i, tx_scan_info_t &tx_scan_info) const;
void parse_block_round(const cryptonote::blobdata &blob, cryptonote::block &bl, crypto::hash &bl_id, bool &error) const;
uint64_t get_upper_transaction_size_limit();
@@ -893,12 +934,15 @@ namespace tools
void set_unspent(size_t idx);
void get_outs(std::vector<std::vector<get_outs_entry>> &outs, const std::vector<size_t> &selected_transfers, size_t fake_outputs_count);
bool tx_add_fake_output(std::vector<std::vector<tools::wallet2::get_outs_entry>> &outs, uint64_t global_index, const crypto::public_key& tx_public_key, const rct::key& mask, uint64_t real_index, bool unlocked) const;
- bool wallet_generate_key_image_helper(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);
crypto::public_key get_tx_pub_key_from_received_outs(const tools::wallet2::transfer_details &td) const;
bool should_pick_a_second_output(bool use_rct, size_t n_transfers, const std::vector<size_t> &unused_transfers_indices, const std::vector<size_t> &unused_dust_indices) const;
std::vector<size_t> get_only_rct(const std::vector<size_t> &unused_dust_indices, const std::vector<size_t> &unused_transfers_indices) const;
void 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);
void trim_hashchain();
+ rct::multisig_kLRki get_multisig_composite_LRki(size_t n, const rct::key &k) const;
+ rct::multisig_kLRki get_multisig_LRki(size_t n, const rct::key &k) const;
+ rct::key get_multisig_k(size_t idx) const;
+ void update_multisig_rescan_info(const std::vector<rct::key> &multisig_k, const std::vector<std::vector<tools::wallet2::multisig_info>> &info, size_t n);
cryptonote::account_base m_account;
boost::optional<epee::net_utils::http::login> m_daemon_login;
@@ -927,6 +971,8 @@ namespace tools
std::unordered_map<std::string, std::string> m_attributes;
std::vector<tools::wallet2::address_book_row> m_address_book;
uint64_t m_upper_transaction_size_limit; //TODO: auto-calc this value or request from daemon, now use some fixed value
+ const std::vector<std::vector<tools::wallet2::multisig_info>> *m_multisig_rescan_info;
+ const std::vector<rct::key> *m_multisig_rescan_k;
std::atomic<bool> m_run;
@@ -976,7 +1022,9 @@ namespace tools
};
}
BOOST_CLASS_VERSION(tools::wallet2, 22)
-BOOST_CLASS_VERSION(tools::wallet2::transfer_details, 8)
+BOOST_CLASS_VERSION(tools::wallet2::transfer_details, 9)
+BOOST_CLASS_VERSION(tools::wallet2::multisig_info, 1)
+BOOST_CLASS_VERSION(tools::wallet2::multisig_tx_set, 1)
BOOST_CLASS_VERSION(tools::wallet2::payment_details, 2)
BOOST_CLASS_VERSION(tools::wallet2::pool_payment_details, 1)
BOOST_CLASS_VERSION(tools::wallet2::unconfirmed_transfer_details, 7)
@@ -985,7 +1033,7 @@ BOOST_CLASS_VERSION(tools::wallet2::address_book_row, 17)
BOOST_CLASS_VERSION(tools::wallet2::unsigned_tx_set, 0)
BOOST_CLASS_VERSION(tools::wallet2::signed_tx_set, 0)
BOOST_CLASS_VERSION(tools::wallet2::tx_construction_data, 2)
-BOOST_CLASS_VERSION(tools::wallet2::pending_tx, 2)
+BOOST_CLASS_VERSION(tools::wallet2::pending_tx, 3)
namespace boost
{
@@ -1023,6 +1071,12 @@ namespace boost
{
x.m_subaddr_index = {};
}
+ if (ver < 9)
+ {
+ x.m_key_image_partial = false;
+ x.m_multisig_info.clear();
+ x.m_multisig_k = rct::zero();
+ }
}
template <class Archive>
@@ -1096,6 +1150,29 @@ namespace boost
return;
}
a & x.m_subaddr_index;
+ if (ver < 9)
+ {
+ initialize_transfer_details(a, x, ver);
+ return;
+ }
+ a & x.m_multisig_info;
+ a & x.m_multisig_k;
+ a & x.m_key_image_partial;
+ }
+
+ template <class Archive>
+ inline void serialize(Archive &a, tools::wallet2::multisig_info &x, const boost::serialization::version_type ver)
+ {
+ a & x.m_partial_key_image;
+ a & x.m_L;
+ a & x.m_R;
+ }
+
+ template <class Archive>
+ inline void serialize(Archive &a, tools::wallet2::multisig_tx_set &x, const boost::serialization::version_type ver)
+ {
+ a & x.m_ptx;
+ a & x.m_signers;
}
template <class Archive>
@@ -1301,6 +1378,9 @@ namespace boost
if (ver < 2)
return;
a & x.selected_transfers;
+ if (ver < 3)
+ return;
+ a & x.msout;
}
}
}
@@ -1478,6 +1558,10 @@ namespace tools
src.real_out_tx_key = get_tx_pub_key_from_extra(td.m_tx);
src.real_output = interted_it - 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);
++i;
}
@@ -1504,7 +1588,8 @@ namespace tools
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, sources, splitted_dsts, change_dts.addr, extra, tx, unlock_time, tx_key, additional_tx_keys);
+ rct::multisig_out msout;
+ 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);
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);
@@ -1532,6 +1617,7 @@ namespace tools
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;
diff --git a/src/wallet/wallet_errors.h b/src/wallet/wallet_errors.h
index 48fce40dd..234c22d85 100644
--- a/src/wallet/wallet_errors.h
+++ b/src/wallet/wallet_errors.h
@@ -50,6 +50,8 @@ namespace tools
// wallet_internal_error
// unexpected_txin_type
// wallet_not_initialized
+ // multisig_export_needed
+ // multisig_import_needed
// std::logic_error
// wallet_logic_error *
// file_exists
@@ -186,7 +188,22 @@ namespace tools
{
}
};
-
+ //----------------------------------------------------------------------------------------------------
+ struct multisig_export_needed : public wallet_runtime_error
+ {
+ explicit multisig_export_needed(std::string&& loc)
+ : wallet_runtime_error(std::move(loc), "This signature was made with stale data: export fresh multisig data, which other participants must then use")
+ {
+ }
+ };
+ //----------------------------------------------------------------------------------------------------
+ struct multisig_import_needed : public wallet_runtime_error
+ {
+ explicit multisig_import_needed(std::string&& loc)
+ : wallet_runtime_error(std::move(loc), "Not enough multisig data was found to sign: import multisig data from more other participants")
+ {
+ }
+ };
//----------------------------------------------------------------------------------------------------
const char* const file_error_messages[] = {
"file already exists",
diff --git a/src/wallet/wallet_rpc_server.cpp b/src/wallet/wallet_rpc_server.cpp
index e790b9954..ab911d0ef 100644
--- a/src/wallet/wallet_rpc_server.cpp
+++ b/src/wallet/wallet_rpc_server.cpp
@@ -324,6 +324,7 @@ namespace tools
{
res.balance = m_wallet->balance(req.account_index);
res.unlocked_balance = m_wallet->unlocked_balance(req.account_index);
+ res.multisig_import_needed = m_wallet->multisig() && m_wallet->has_multisig_partial_key_images();
std::map<uint32_t, uint64_t> balance_per_subaddress = m_wallet->balance_per_subaddress(req.account_index);
std::map<uint32_t, uint64_t> unlocked_balance_per_subaddress = m_wallet->unlocked_balance_per_subaddress(req.account_index);
std::vector<tools::wallet2::transfer_details> transfers;
diff --git a/src/wallet/wallet_rpc_server_commands_defs.h b/src/wallet/wallet_rpc_server_commands_defs.h
index e084d9e6d..cf35cc48d 100644
--- a/src/wallet/wallet_rpc_server_commands_defs.h
+++ b/src/wallet/wallet_rpc_server_commands_defs.h
@@ -78,11 +78,13 @@ namespace wallet_rpc
{
uint64_t balance;
uint64_t unlocked_balance;
+ bool multisig_import_needed;
std::vector<per_subaddress_info> per_subaddress;
BEGIN_KV_SERIALIZE_MAP()
KV_SERIALIZE(balance)
KV_SERIALIZE(unlocked_balance)
+ KV_SERIALIZE(multisig_import_needed)
KV_SERIALIZE(per_subaddress)
END_KV_SERIALIZE_MAP()
};