diff options
author | Riccardo Spagni <ric@spagni.net> | 2017-12-17 21:05:16 +0200 |
---|---|---|
committer | Riccardo Spagni <ric@spagni.net> | 2017-12-17 21:05:16 +0200 |
commit | 1cc745113015dda0710e966fceb351e1f50c62e0 (patch) | |
tree | 168da14eda885978e872d247e435f7ea6475d3ee /src/wallet | |
parent | Merge pull request #2950 (diff) | |
parent | change the N-1/N multisig second message signer for auth (diff) | |
download | monero-1cc745113015dda0710e966fceb351e1f50c62e0.tar.xz |
Merge pull request #2134
ceabc4f9 change the N-1/N multisig second message signer for auth (moneromooo-monero)
55c2845d core_tests: multisig test now tests multiple inputs (moneromooo-monero)
98db7ee4 wallet: factor multisig info parsing (moneromooo-monero)
31a97e76 wallet: use raw encrypted data in multisig import/export RPC (moneromooo-monero)
2fa707d1 wallet: add multisig sign/submit RPC (moneromooo-monero)
e36f5b60 Match surae's recommendation to derive multisig keys (moneromooo-monero)
a36c261d wallet2: fix slow multisig unit tests with subaddress patch (moneromooo-monero)
fa569712 make multisig work with subaddresses (moneromooo-monero)
dffa0dce simplewallet: add export_raw_multisig command (moneromooo-monero)
7f4c220b simplewallet: add multisig to wallet type in wallet_info output (moneromooo-monero)
26529038 wallet: guard against partly initialized multisig wallet (moneromooo-monero)
66e34e85 add multisig core test and factor multisig building blocks (moneromooo-monero)
f4eda44c N-1/N multisig (moneromooo-monero)
cd64c799 multisig address generation RPC (moneromooo-monero)
fff871a4 gen_multisig: generates multisig wallets if participants trust each other (moneromooo-monero)
95a21a79 wallet2: allow empty wallet filename to avoid saving data (moneromooo-monero)
b84b3565 tests: add multisig unit tests (moneromooo-monero)
4c313324 Add N/N multisig tx generation and signing (moneromooo-monero)
6d219a92 wallet: add multisig key generation (moneromooo-monero)
Diffstat (limited to 'src/wallet')
-rw-r--r-- | src/wallet/CMakeLists.txt | 2 | ||||
-rw-r--r-- | src/wallet/api/wallet2_api.h | 4 | ||||
-rw-r--r-- | src/wallet/api/wallet_manager.cpp | 4 | ||||
-rw-r--r-- | src/wallet/api/wallet_manager.h | 2 | ||||
-rw-r--r-- | src/wallet/wallet2.cpp | 1339 | ||||
-rw-r--r-- | src/wallet/wallet2.h | 201 | ||||
-rw-r--r-- | src/wallet/wallet_args.cpp | 4 | ||||
-rw-r--r-- | src/wallet/wallet_args.h | 1 | ||||
-rw-r--r-- | src/wallet/wallet_errors.h | 19 | ||||
-rw-r--r-- | src/wallet/wallet_rpc_server.cpp | 680 | ||||
-rw-r--r-- | src/wallet/wallet_rpc_server.h | 16 | ||||
-rw-r--r-- | src/wallet/wallet_rpc_server_commands_defs.h | 188 | ||||
-rw-r--r-- | src/wallet/wallet_rpc_server_error_codes.h | 9 |
13 files changed, 2283 insertions, 186 deletions
diff --git a/src/wallet/CMakeLists.txt b/src/wallet/CMakeLists.txt index ab48bd7a2..2d664ba15 100644 --- a/src/wallet/CMakeLists.txt +++ b/src/wallet/CMakeLists.txt @@ -51,6 +51,7 @@ monero_add_library(wallet ${wallet_private_headers}) target_link_libraries(wallet PUBLIC + multisig common cryptonote_core mnemonics @@ -104,6 +105,7 @@ if (BUILD_GUI_DEPS) set(libs_to_merge wallet_api wallet + multisig cryptonote_core cryptonote_basic mnemonics 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 abd24295a..04c6ee236 100644 --- a/src/wallet/wallet2.cpp +++ b/src/wallet/wallet2.cpp @@ -46,6 +46,7 @@ using namespace epee; #include "rpc/core_rpc_server_commands_defs.h" #include "misc_language.h" #include "cryptonote_basic/cryptonote_basic_impl.h" +#include "multisig/multisig.h" #include "common/boost_serialization_helper.h" #include "common/command_line.h" #include "common/threadpool.h" @@ -56,6 +57,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 +89,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) @@ -100,6 +103,8 @@ using namespace cryptonote; #define KEY_IMAGE_EXPORT_FILE_MAGIC "Monero key image export\002" +#define MULTISIG_EXPORT_FILE_MAGIC "Monero multisig export\001" + namespace { // Create on-demand to prevent static initialization order fiasco issues. @@ -524,6 +529,42 @@ uint8_t get_bulletproof_fork(bool testnet) return 255; // TODO } +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 @@ -534,6 +575,41 @@ const size_t MAX_SPLIT_ATTEMPTS = 30; constexpr const std::chrono::seconds wallet2::rpc_timeout; const char* wallet2::tr(const char* str) { return i18n_translate(str, "tools::wallet2"); } +wallet2::wallet2(bool testnet, bool restricted): + 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(RefreshOptimizeCoinbase), + 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_subaddress_lookahead_major(SUBADDRESS_LOOKAHEAD_MAJOR), + m_subaddress_lookahead_minor(SUBADDRESS_LOOKAHEAD_MINOR), + 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) +{ +} + bool wallet2::has_testnet_option(const boost::program_options::variables_map& vm) { return command_line::get_arg(vm, options().testnet); @@ -725,9 +801,9 @@ void wallet2::expand_subaddresses(const cryptonote::subaddress_index& index) { // add new accounts cryptonote::subaddress_index index2; - for (index2.major = m_subaddress_labels.size(); index2.major < index.major + SUBADDRESS_LOOKAHEAD_MAJOR; ++index2.major) + for (index2.major = m_subaddress_labels.size(); index2.major < index.major + m_subaddress_lookahead_major; ++index2.major) { - for (index2.minor = 0; index2.minor < (index2.major == index.major ? index.minor : 0) + SUBADDRESS_LOOKAHEAD_MINOR; ++index2.minor) + for (index2.minor = 0; index2.minor < (index2.major == index.major ? index.minor : 0) + m_subaddress_lookahead_minor; ++index2.minor) { if (m_subaddresses_inv.count(index2) == 0) { @@ -744,7 +820,7 @@ void wallet2::expand_subaddresses(const cryptonote::subaddress_index& index) { // add new subaddresses cryptonote::subaddress_index index2 = index; - for (index2.minor = m_subaddress_labels[index.major].size(); index2.minor < index.minor + SUBADDRESS_LOOKAHEAD_MINOR; ++index2.minor) + for (index2.minor = m_subaddress_labels[index.major].size(); index2.minor < index.minor + m_subaddress_lookahead_minor; ++index2.minor) { if (m_subaddresses_inv.count(index2) == 0) { @@ -774,6 +850,12 @@ void wallet2::set_subaddress_label(const cryptonote::subaddress_index& index, co m_subaddress_labels[index.major][index.minor] = label; } //---------------------------------------------------------------------------------------------------- +void wallet2::set_subaddress_lookahead(size_t major, size_t minor) +{ + m_subaddress_lookahead_major = major; + m_subaddress_lookahead_minor = minor; +} +//---------------------------------------------------------------------------------------------------- /*! * \brief Tells if the wallet file is deprecated. */ @@ -846,10 +928,20 @@ 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); - 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"); + THROW_WALLET_EXCEPTION_IF(i >= tx.vout.size(), error::wallet_internal_error, "Invalid vout index"); + if (m_multisig) + { + tx_scan_info.in_ephemeral.pub = boost::get<cryptonote::txout_to_key>(tx.vout[i].target).key; + tx_scan_info.in_ephemeral.sec = crypto::null_skey; + tx_scan_info.ki = rct::rct2ki(rct::zero()); + } + else + { + 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); + 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"); + } outs.push_back(i); if (tx_scan_info.money_transfered == 0) @@ -1018,7 +1110,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 +1133,16 @@ 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); + } 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 +1191,13 @@ 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); + } 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 +2191,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); } @@ -2157,6 +2267,7 @@ bool wallet2::clear() bool wallet2::store_keys(const std::string& keys_file_name, const epee::wipeable_string& password, bool watch_only) { std::string account_data; + std::string multisig_signers; cryptonote::account_base account = m_account; if (watch_only) @@ -2181,6 +2292,20 @@ bool wallet2::store_keys(const std::string& keys_file_name, const epee::wipeable value2.SetInt(watch_only ? 1 :0); // WTF ? JSON has different true and false types, and not boolean ?? json.AddMember("watch_only", value2, json.GetAllocator()); + value2.SetInt(m_multisig ? 1 :0); + json.AddMember("multisig", value2, json.GetAllocator()); + + value2.SetUint(m_multisig_threshold); + json.AddMember("multisig_threshold", value2, json.GetAllocator()); + + if (m_multisig) + { + bool r = ::serialization::dump_binary(m_multisig_signers, multisig_signers); + CHECK_AND_ASSERT_MES(r, false, "failed to serialize wallet multisig signers"); + value.SetString(multisig_signers.c_str(), multisig_signers.length()); + json.AddMember("multisig_signers", value, json.GetAllocator()); + } + value2.SetInt(m_always_confirm_transfers ? 1 :0); json.AddMember("always_confirm_transfers", value2, json.GetAllocator()); @@ -2292,6 +2417,9 @@ bool wallet2::load_keys(const std::string& keys_file_name, const epee::wipeable_ { is_old_file_format = true; m_watch_only = false; + m_multisig = false; + m_multisig_threshold = 0; + m_multisig_signers.clear(); m_always_confirm_transfers = false; m_print_ring_members = false; m_default_mixin = 0; @@ -2328,6 +2456,31 @@ bool wallet2::load_keys(const std::string& keys_file_name, const epee::wipeable_ } GET_FIELD_FROM_JSON_RETURN_ON_ERROR(json, watch_only, int, Int, false, false); m_watch_only = field_watch_only; + GET_FIELD_FROM_JSON_RETURN_ON_ERROR(json, multisig, int, Int, false, false); + m_multisig = field_multisig; + GET_FIELD_FROM_JSON_RETURN_ON_ERROR(json, multisig_threshold, unsigned int, Uint, m_multisig, 0); + m_multisig_threshold = field_multisig_threshold; + if (m_multisig) + { + if (!json.HasMember("multisig_signers")) + { + LOG_ERROR("Field multisig_signers not found in JSON"); + return false; + } + if (!json["multisig_signers"].IsString()) + { + LOG_ERROR("Field multisig_signers found in JSON, but not String"); + return false; + } + const char *field_multisig_signers = json["multisig_signers"].GetString(); + std::string multisig_signers = std::string(field_multisig_signers, field_multisig_signers + json["multisig_signers"].GetStringLength()); + r = ::serialization::parse_binary(multisig_signers, m_multisig_signers); + if (!r) + { + LOG_ERROR("Field multisig_signers found in JSON, but failed to parse"); + return false; + } + } GET_FIELD_FROM_JSON_RETURN_ON_ERROR(json, always_confirm_transfers, int, Int, false, true); m_always_confirm_transfers = field_always_confirm_transfers; GET_FIELD_FROM_JSON_RETURN_ON_ERROR(json, print_ring_members, int, Int, false, true); @@ -2394,7 +2547,7 @@ bool wallet2::load_keys(const std::string& keys_file_name, const epee::wipeable_ const cryptonote::account_keys& keys = m_account.get_keys(); r = epee::serialization::load_t_from_binary(m_account, account_data); r = r && verify_keys(keys.m_view_secret_key, keys.m_account_address.m_view_public_key); - if(!m_watch_only) + if(!m_watch_only && !m_multisig) r = r && verify_keys(keys.m_spend_secret_key, keys.m_account_address.m_spend_public_key); THROW_WALLET_EXCEPTION_IF(!r, error::invalid_password); return true; @@ -2412,14 +2565,14 @@ bool wallet2::load_keys(const std::string& keys_file_name, const epee::wipeable_ */ bool wallet2::verify_password(const epee::wipeable_string& password) const { - return verify_password(m_keys_file, password, m_watch_only); + return verify_password(m_keys_file, password, m_watch_only || m_multisig); } /*! * \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 @@ -2427,7 +2580,7 @@ bool wallet2::verify_password(const epee::wipeable_string& password) const * can be used prior to rewriting wallet keys file, to ensure user has entered the correct password * */ -bool wallet2::verify_password(const std::string& keys_file_name, const epee::wipeable_string& password, bool watch_only) +bool wallet2::verify_password(const std::string& keys_file_name, const epee::wipeable_string& password, bool no_spend_key) { wallet2::keys_file_data keys_file_data; std::string buf; @@ -2461,7 +2614,7 @@ bool wallet2::verify_password(const std::string& keys_file_name, const epee::wip const cryptonote::account_keys& keys = account_data_check.get_keys(); r = r && verify_keys(keys.m_view_secret_key, keys.m_account_address.m_view_public_key); - if(!watch_only) + if(!no_spend_key) r = r && verify_keys(keys.m_spend_secret_key, keys.m_account_address.m_spend_public_key); return r; } @@ -2481,14 +2634,20 @@ crypto::secret_key wallet2::generate(const std::string& wallet_, const epee::wip clear(); prepare_file_names(wallet_); - boost::system::error_code ignored_ec; - THROW_WALLET_EXCEPTION_IF(boost::filesystem::exists(m_wallet_file, ignored_ec), error::file_exists, m_wallet_file); - THROW_WALLET_EXCEPTION_IF(boost::filesystem::exists(m_keys_file, ignored_ec), error::file_exists, m_keys_file); + if (!wallet_.empty()) + { + boost::system::error_code ignored_ec; + THROW_WALLET_EXCEPTION_IF(boost::filesystem::exists(m_wallet_file, ignored_ec), error::file_exists, m_wallet_file); + THROW_WALLET_EXCEPTION_IF(boost::filesystem::exists(m_keys_file, ignored_ec), error::file_exists, m_keys_file); + } crypto::secret_key retval = m_account.generate(recovery_param, recover, two_random); m_account_public_address = m_account.get_keys().m_account_address; m_watch_only = false; + m_multisig = false; + m_multisig_threshold = 0; + m_multisig_signers.clear(); // -1 month for fluctuations in block time and machine date/time setup. // avg seconds per block @@ -2502,18 +2661,23 @@ crypto::secret_key wallet2::generate(const std::string& wallet_, const epee::wip m_refresh_from_block_height = height >= blocks_per_month ? height - blocks_per_month : 0; } - bool r = store_keys(m_keys_file, password, false); - THROW_WALLET_EXCEPTION_IF(!r, error::file_save_error, m_keys_file); + if (!wallet_.empty()) + { + bool r = store_keys(m_keys_file, password, false); + THROW_WALLET_EXCEPTION_IF(!r, error::file_save_error, m_keys_file); - r = file_io_utils::save_string_to_file(m_wallet_file + ".address.txt", m_account.get_public_address_str(m_testnet)); - if(!r) MERROR("String with address text not saved"); + r = file_io_utils::save_string_to_file(m_wallet_file + ".address.txt", m_account.get_public_address_str(m_testnet)); + if(!r) MERROR("String with address text not saved"); + } cryptonote::block b; generate_genesis(b); m_blockchain.push_back(get_block_hash(b)); add_subaddress_account(tr("Primary account")); - store(); + if (!wallet_.empty()) + store(); + return retval; } @@ -2562,26 +2726,36 @@ void wallet2::generate(const std::string& wallet_, const epee::wipeable_string& clear(); prepare_file_names(wallet_); - boost::system::error_code ignored_ec; - THROW_WALLET_EXCEPTION_IF(boost::filesystem::exists(m_wallet_file, ignored_ec), error::file_exists, m_wallet_file); - THROW_WALLET_EXCEPTION_IF(boost::filesystem::exists(m_keys_file, ignored_ec), error::file_exists, m_keys_file); + if (!wallet_.empty()) + { + boost::system::error_code ignored_ec; + THROW_WALLET_EXCEPTION_IF(boost::filesystem::exists(m_wallet_file, ignored_ec), error::file_exists, m_wallet_file); + THROW_WALLET_EXCEPTION_IF(boost::filesystem::exists(m_keys_file, ignored_ec), error::file_exists, m_keys_file); + } m_account.create_from_viewkey(account_public_address, viewkey); m_account_public_address = account_public_address; m_watch_only = true; + m_multisig = false; + m_multisig_threshold = 0; + m_multisig_signers.clear(); - bool r = store_keys(m_keys_file, password, true); - THROW_WALLET_EXCEPTION_IF(!r, error::file_save_error, m_keys_file); + if (!wallet_.empty()) + { + bool r = store_keys(m_keys_file, password, true); + THROW_WALLET_EXCEPTION_IF(!r, error::file_save_error, m_keys_file); - r = file_io_utils::save_string_to_file(m_wallet_file + ".address.txt", m_account.get_public_address_str(m_testnet)); - if(!r) MERROR("String with address text not saved"); + r = file_io_utils::save_string_to_file(m_wallet_file + ".address.txt", m_account.get_public_address_str(m_testnet)); + if(!r) MERROR("String with address text not saved"); + } cryptonote::block b; generate_genesis(b); m_blockchain.push_back(get_block_hash(b)); add_subaddress_account(tr("Primary account")); - store(); + if (!wallet_.empty()) + store(); } /*! @@ -2598,26 +2772,373 @@ void wallet2::generate(const std::string& wallet_, const epee::wipeable_string& clear(); prepare_file_names(wallet_); - boost::system::error_code ignored_ec; - THROW_WALLET_EXCEPTION_IF(boost::filesystem::exists(m_wallet_file, ignored_ec), error::file_exists, m_wallet_file); - THROW_WALLET_EXCEPTION_IF(boost::filesystem::exists(m_keys_file, ignored_ec), error::file_exists, m_keys_file); + if (!wallet_.empty()) + { + boost::system::error_code ignored_ec; + THROW_WALLET_EXCEPTION_IF(boost::filesystem::exists(m_wallet_file, ignored_ec), error::file_exists, m_wallet_file); + THROW_WALLET_EXCEPTION_IF(boost::filesystem::exists(m_keys_file, ignored_ec), error::file_exists, m_keys_file); + } m_account.create_from_keys(account_public_address, spendkey, viewkey); m_account_public_address = account_public_address; m_watch_only = false; + m_multisig = false; + m_multisig_threshold = 0; + m_multisig_signers.clear(); - bool r = store_keys(m_keys_file, password, false); - THROW_WALLET_EXCEPTION_IF(!r, error::file_save_error, m_keys_file); + if (!wallet_.empty()) + { + bool r = store_keys(m_keys_file, password, false); + THROW_WALLET_EXCEPTION_IF(!r, error::file_save_error, m_keys_file); - r = file_io_utils::save_string_to_file(m_wallet_file + ".address.txt", m_account.get_public_address_str(m_testnet)); - if(!r) MERROR("String with address text not saved"); + r = file_io_utils::save_string_to_file(m_wallet_file + ".address.txt", m_account.get_public_address_str(m_testnet)); + if(!r) MERROR("String with address text not saved"); + } + + cryptonote::block b; + generate_genesis(b); + m_blockchain.push_back(get_block_hash(b)); + + if (!wallet_.empty()) + store(); +} + +std::string wallet2::make_multisig(const epee::wipeable_string &password, + const std::vector<crypto::secret_key> &view_keys, + const std::vector<crypto::public_key> &spend_keys, + uint32_t threshold) +{ + 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 > 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"); + + std::string extra_multisig_info; + crypto::hash hash; + + clear(); + + MINFO("Creating spend key..."); + std::vector<crypto::secret_key> multisig_keys; + rct::key spend_pkey, spend_skey; + if (threshold == spend_keys.size() + 1) + { + cryptonote::generate_multisig_N_N(get_account().get_keys(), spend_keys, multisig_keys, spend_skey, spend_pkey); + } + else if (threshold == spend_keys.size()) + { + cryptonote::generate_multisig_N1_N(get_account().get_keys(), spend_keys, multisig_keys, spend_skey, spend_pkey); + + // We need an extra step, so we package all the composite public keys + // we know about, and make a signed string out of them + std::string data; + crypto::public_key signer; + CHECK_AND_ASSERT_THROW_MES(crypto::secret_key_to_public_key(rct::rct2sk(spend_skey), signer), "Failed to derive public spend key"); + data += std::string((const char *)&signer, sizeof(crypto::public_key)); + + for (const auto &msk: multisig_keys) + { + rct::key pmsk = rct::scalarmultBase(rct::sk2rct(msk)); + data += std::string((const char *)&pmsk, sizeof(crypto::public_key)); + } + + data.resize(data.size() + sizeof(crypto::signature)); + 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, signer, rct::rct2sk(spend_skey), signature); + + extra_multisig_info = std::string("MultisigxV1") + tools::base58::encode(data); + } + else + { + CHECK_AND_ASSERT_THROW_MES(false, "Unsupported threshold case"); + } + + // the multisig view key is shared by all, make one all can derive + MINFO("Creating view key..."); + crypto::secret_key view_skey = cryptonote::generate_multisig_view_secret_key(get_account().get_keys().m_view_secret_key, view_keys); + + MINFO("Creating multisig address..."); + CHECK_AND_ASSERT_THROW_MES(m_account.make_multisig(view_skey, rct::rct2sk(spend_skey), rct::rct2pk(spend_pkey), multisig_keys), + "Failed to create multisig wallet due to bad keys"); + + m_account_public_address = m_account.get_keys().m_account_address; + m_watch_only = false; + m_multisig = true; + m_multisig_threshold = threshold; + if (threshold == spend_keys.size() + 1) + { + m_multisig_signers = spend_keys; + m_multisig_signers.push_back(get_multisig_signer_public_key()); + } + else + { + m_multisig_signers = std::vector<crypto::public_key>(spend_keys.size() + 1, crypto::null_pkey); + } + + if (!m_wallet_file.empty()) + { + bool r = store_keys(m_keys_file, password, false); + THROW_WALLET_EXCEPTION_IF(!r, error::file_save_error, m_keys_file); + + r = file_io_utils::save_string_to_file(m_wallet_file + ".address.txt", m_account.get_public_address_str(m_testnet)); + if(!r) MERROR("String with address text not saved"); + } cryptonote::block b; generate_genesis(b); m_blockchain.push_back(get_block_hash(b)); add_subaddress_account(tr("Primary account")); - store(); + if (!m_wallet_file.empty()) + store(); + + return extra_multisig_info; +} + +std::string wallet2::make_multisig(const epee::wipeable_string &password, + const std::vector<std::string> &info, + uint32_t threshold) +{ + // parse all multisig info + std::vector<crypto::secret_key> secret_keys(info.size()); + std::vector<crypto::public_key> public_keys(info.size()); + for (size_t i = 0; i < info.size(); ++i) + { + THROW_WALLET_EXCEPTION_IF(!verify_multisig_info(info[i], secret_keys[i], public_keys[i]), + error::wallet_internal_error, "Bad multisig info: " + info[i]); + } + + // remove duplicates + for (size_t i = 0; i < secret_keys.size(); ++i) + { + for (size_t j = i + 1; j < secret_keys.size(); ++j) + { + if (rct::sk2rct(secret_keys[i]) == rct::sk2rct(secret_keys[j])) + { + MDEBUG("Duplicate key found, ignoring"); + secret_keys[j] = secret_keys.back(); + public_keys[j] = public_keys.back(); + secret_keys.pop_back(); + public_keys.pop_back(); + --j; + } + } + } + + // people may include their own, weed it out + const crypto::secret_key local_skey = cryptonote::get_multisig_blinded_secret_key(get_account().get_keys().m_view_secret_key); + const crypto::public_key local_pkey = get_multisig_signer_public_key(get_account().get_keys().m_spend_secret_key); + for (size_t i = 0; i < secret_keys.size(); ++i) + { + if (secret_keys[i] == local_skey) + { + MDEBUG("Local key is present, ignoring"); + secret_keys[i] = secret_keys.back(); + public_keys[i] = public_keys.back(); + secret_keys.pop_back(); + public_keys.pop_back(); + --i; + } + else + { + THROW_WALLET_EXCEPTION_IF(public_keys[i] == local_pkey, error::wallet_internal_error, + "Found local spend public key, but not local view secret key - something very weird"); + } + } + + return make_multisig(password, secret_keys, public_keys, threshold); +} + +bool wallet2::finalize_multisig(const epee::wipeable_string &password, std::unordered_set<crypto::public_key> pkeys, std::vector<crypto::public_key> signers) +{ + CHECK_AND_ASSERT_THROW_MES(!pkeys.empty(), "empty pkeys"); + + // add ours if not included + crypto::public_key local_signer; + CHECK_AND_ASSERT_THROW_MES(crypto::secret_key_to_public_key(get_account().get_keys().m_spend_secret_key, local_signer), + "Failed to derive public spend key"); + if (std::find(signers.begin(), signers.end(), local_signer) == signers.end()) + { + signers.push_back(local_signer); + for (const auto &msk: get_account().get_multisig_keys()) + { + pkeys.insert(rct::rct2pk(rct::scalarmultBase(rct::sk2rct(msk)))); + } + } + + CHECK_AND_ASSERT_THROW_MES(signers.size() == m_multisig_signers.size(), "Bad signers size"); + + crypto::public_key spend_public_key = cryptonote::generate_multisig_N1_N_spend_public_key(std::vector<crypto::public_key>(pkeys.begin(), pkeys.end())); + m_account_public_address.m_spend_public_key = spend_public_key; + m_account.finalize_multisig(spend_public_key); + + m_multisig_signers = signers; + std::sort(m_multisig_signers.begin(), m_multisig_signers.end(), [](const crypto::public_key &e0, const crypto::public_key &e1){ return memcmp(&e0, &e1, sizeof(e0)); }); + + if (!m_wallet_file.empty()) + { + bool r = store_keys(m_keys_file, password, false); + THROW_WALLET_EXCEPTION_IF(!r, error::file_save_error, m_keys_file); + + r = file_io_utils::save_string_to_file(m_wallet_file + ".address.txt", m_account.get_public_address_str(m_testnet)); + if(!r) MERROR("String with address text not saved"); + } + + m_subaddresses.clear(); + m_subaddresses_inv.clear(); + m_subaddress_labels.clear(); + add_subaddress_account(tr("Primary account")); + + if (!m_wallet_file.empty()) + store(); + + return true; +} + +bool wallet2::finalize_multisig(const epee::wipeable_string &password, const std::vector<std::string> &info) +{ + // parse all multisig info + std::unordered_set<crypto::public_key> public_keys; + std::vector<crypto::public_key> signers(info.size(), crypto::null_pkey); + for (size_t i = 0; i < info.size(); ++i) + { + if (!verify_extra_multisig_info(info[i], public_keys, signers[i])) + { + MERROR("Bad multisig info"); + return false; + } + } + return finalize_multisig(password, public_keys, signers); +} + +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 = cryptonote::get_multisig_blinded_secret_key(get_account().get_keys().m_view_secret_key); + const crypto::public_key pkey = get_multisig_signer_public_key(get_account().get_keys().m_spend_secret_key); + crypto::hash hash; + + std::string data; + data += std::string((const char *)&skey, sizeof(crypto::secret_key)); + data += std::string((const char *)&pkey, sizeof(crypto::public_key)); + + data.resize(data.size() + sizeof(crypto::signature)); + 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_multisig_blinded_secret_key(get_account().get_keys().m_spend_secret_key), signature); + + return std::string("MultisigV1") + tools::base58::encode(data); +} + +bool wallet2::verify_multisig_info(const std::string &data, crypto::secret_key &skey, crypto::public_key &pkey) +{ + const size_t header_len = strlen("MultisigV1"); + if (data.size() < header_len || data.substr(0, header_len) != "MultisigV1") + { + MERROR("Multisig info header check error"); + return false; + } + std::string decoded; + if (!tools::base58::decode(data.substr(header_len), decoded)) + { + MERROR("Multisig info decoding error"); + return false; + } + if (decoded.size() != sizeof(crypto::secret_key) + sizeof(crypto::public_key) + sizeof(crypto::signature)) + { + MERROR("Multisig info is corrupt"); + return false; + } + + size_t offset = 0; + skey = *(const crypto::secret_key*)(decoded.data() + offset); + offset += sizeof(skey); + pkey = *(const crypto::public_key*)(decoded.data() + offset); + offset += sizeof(pkey); + const crypto::signature &signature = *(const crypto::signature*)(decoded.data() + offset); + + crypto::hash hash; + crypto::cn_fast_hash(decoded.data(), decoded.size() - sizeof(signature), hash); + if (!crypto::check_signature(hash, pkey, signature)) + { + MERROR("Multisig info signature is invalid"); + return false; + } + + return true; +} + +bool wallet2::verify_extra_multisig_info(const std::string &data, std::unordered_set<crypto::public_key> &pkeys, crypto::public_key &signer) +{ + const size_t header_len = strlen("MultisigxV1"); + if (data.size() < header_len || data.substr(0, header_len) != "MultisigxV1") + { + MERROR("Multisig info header check error"); + return false; + } + std::string decoded; + if (!tools::base58::decode(data.substr(header_len), decoded)) + { + MERROR("Multisig info decoding error"); + return false; + } + if (decoded.size() < sizeof(crypto::public_key) + sizeof(crypto::signature)) + { + MERROR("Multisig info is corrupt"); + return false; + } + if ((decoded.size() - (sizeof(crypto::public_key) + sizeof(crypto::signature))) % sizeof(crypto::public_key)) + { + MERROR("Multisig info is corrupt"); + return false; + } + + const size_t n_keys = (decoded.size() - (sizeof(crypto::public_key) + sizeof(crypto::signature))) / sizeof(crypto::public_key); + size_t offset = 0; + signer = *(const crypto::public_key*)(decoded.data() + offset); + offset += sizeof(signer); + const crypto::signature &signature = *(const crypto::signature*)(decoded.data() + offset + n_keys * sizeof(crypto::public_key)); + + crypto::hash hash; + crypto::cn_fast_hash(decoded.data(), decoded.size() - sizeof(signature), hash); + if (!crypto::check_signature(hash, signer, signature)) + { + MERROR("Multisig info signature is invalid"); + return false; + } + + for (size_t n = 0; n < n_keys; ++n) + { + crypto::public_key mspk = *(const crypto::public_key*)(decoded.data() + offset); + pkeys.insert(mspk); + offset += sizeof(mspk); + } + + return true; +} + +bool wallet2::multisig(bool *ready, uint32_t *threshold, uint32_t *total) const +{ + if (!m_multisig) + return false; + if (threshold) + *threshold = m_multisig_threshold; + if (total) + *total = m_multisig_signers.size(); + if (ready) + *ready = !(get_account().get_keys().m_account_address.m_spend_public_key == rct::rct2pk(rct::identity())); + 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; } /*! @@ -2627,6 +3148,8 @@ void wallet2::generate(const std::string& wallet_, const epee::wipeable_string& */ void wallet2::rewrite(const std::string& wallet_name, const epee::wipeable_string& password) { + if (wallet_name.empty()) + return; prepare_file_names(wallet_name); boost::system::error_code ignored_ec; THROW_WALLET_EXCEPTION_IF(!boost::filesystem::exists(m_keys_file, ignored_ec), error::file_not_found, m_keys_file); @@ -3200,7 +3723,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)) { @@ -3540,23 +4063,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) @@ -3624,6 +4130,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.clear(); + //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 @@ -3646,27 +4156,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; @@ -3787,7 +4280,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, @@ -3832,7 +4326,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; } @@ -3958,11 +4452,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; } @@ -3971,6 +4466,292 @@ bool wallet2::load_tx(const std::string &signed_filename, std::vector<tools::wal return true; } //---------------------------------------------------------------------------------------------------- +std::string wallet2::save_multisig_tx(multisig_tx_set txs) +{ + 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.clear(); + + // 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 std::string(); + } + LOG_PRINT_L2("Saving multisig unsigned tx data: " << oss.str()); + std::string ciphertext = encrypt_with_view_secret_key(oss.str()); + return std::string(MULTISIG_UNSIGNED_TX_PREFIX) + ciphertext; +} +//---------------------------------------------------------------------------------------------------- +bool wallet2::save_multisig_tx(const multisig_tx_set &txs, const std::string &filename) +{ + std::string ciphertext = save_multisig_tx(txs); + if (ciphertext.empty()) + return false; + return epee::file_io_utils::save_string_to_file(filename, ciphertext); +} +//---------------------------------------------------------------------------------------------------- +std::string wallet2::save_multisig_tx(const std::vector<pending_tx>& ptx_vector) +{ + multisig_tx_set txs; + txs.m_ptx = ptx_vector; + + for (const auto &msk: get_account().get_multisig_keys()) + { + crypto::public_key pkey = get_multisig_signing_public_key(msk); + for (auto &ptx: txs.m_ptx) for (auto &sig: ptx.multisig_sigs) sig.signing_keys.insert(pkey); + } + + txs.m_signers.insert(get_multisig_signer_public_key()); + + return save_multisig_tx(txs); +} +//---------------------------------------------------------------------------------------------------- +bool wallet2::save_multisig_tx(const std::vector<pending_tx>& ptx_vector, const std::string &filename) +{ + std::string ciphertext = save_multisig_tx(ptx_vector); + if (ciphertext.empty()) + return false; + return epee::file_io_utils::save_string_to_file(filename, ciphertext); +} +//---------------------------------------------------------------------------------------------------- +bool wallet2::load_multisig_tx(cryptonote::blobdata s, multisig_tx_set &exported_txs, std::function<bool(const multisig_tx_set&)> accept_func) +{ + 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 multisig tx data"); + return false; + } + try + { + s = decrypt_with_view_secret_key(std::string(s, magiclen)); + } + catch (const std::exception &e) + { + LOG_PRINT_L0("Failed to decrypt multisig tx data: " << e.what()); + return false; + } + try + { + std::istringstream iss(s); + boost::archive::portable_binary_iarchive ar(iss); + ar >> exported_txs; + } + catch (...) + { + LOG_PRINT_L0("Failed to parse multisig tx data"); + 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::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; + } + + if (!load_multisig_tx(s, exported_txs, accept_func)) + { + LOG_PRINT_L0("Failed to parse multisig tx data from " << filename); + return false; + } + return true; +} +//---------------------------------------------------------------------------------------------------- +bool wallet2::sign_multisig_tx(multisig_tx_set &exported_txs, std::vector<crypto::hash> &txids) +{ + THROW_WALLET_EXCEPTION_IF(exported_txs.m_ptx.empty(), error::wallet_internal_error, "No tx found"); + + const crypto::public_key local_signer = get_multisig_signer_public_key(); + + THROW_WALLET_EXCEPTION_IF(exported_txs.m_signers.find(local_signer) != 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"); + + 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]; + THROW_WALLET_EXCEPTION_IF(ptx.multisig_sigs.empty(), error::wallet_internal_error, "No signatures found in multisig tx"); + 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.multisig_sigs.front().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); + + for (auto &sig: ptx.multisig_sigs) + { + if (sig.ignore != local_signer) + { + ptx.tx.rct_signatures = sig.sigs; + + rct::keyV k; + for (size_t idx: sd.selected_transfers) + k.push_back(get_multisig_k(idx, sig.used_L)); + + rct::key skey = rct::zero(); + for (const auto &msk: get_account().get_multisig_keys()) + { + crypto::public_key pmsk = get_multisig_signing_public_key(msk); + + if (sig.signing_keys.find(pmsk) == sig.signing_keys.end()) + { + sc_add(skey.bytes, skey.bytes, rct::sk2rct(msk).bytes); + sig.signing_keys.insert(pmsk); + } + } + THROW_WALLET_EXCEPTION_IF(!rct::signMultisig(ptx.tx.rct_signatures, indices, k, sig.msout, skey), + error::wallet_internal_error, "Failed signing, transaction likely malformed"); + + sig.sigs = ptx.tx.rct_signatures; + } + } + + const bool is_last = exported_txs.m_signers.size() + 1 >= m_multisig_threshold; + if (is_last) + { + // when the last signature on a multisig tx is made, we select the right + // signature to plug into the final tx + bool found = false; + for (const auto &sig: ptx.multisig_sigs) + { + if (sig.ignore != local_signer && exported_txs.m_signers.find(sig.ignore) == exported_txs.m_signers.end()) + { + THROW_WALLET_EXCEPTION_IF(found, error::wallet_internal_error, "More than one transaction is final"); + ptx.tx.rct_signatures = sig.sigs; + found = true; + } + } + THROW_WALLET_EXCEPTION_IF(!found, error::wallet_internal_error, + "Final signed transaction not found: this transaction was likely made without our export data, so we cannot sign it"); + 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.clear(); + + exported_txs.m_signers.insert(get_multisig_signer_public_key()); + + return true; +} +//---------------------------------------------------------------------------------------------------- +bool wallet2::sign_multisig_tx_to_file(multisig_tx_set &exported_txs, const std::string &filename, std::vector<crypto::hash> &txids) +{ + bool r = sign_multisig_tx(exported_txs, txids); + if (!r) + return false; + 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; + + if (accept_func && !accept_func(exported_txs)) + { + LOG_PRINT_L1("Transactions rejected by callback"); + return false; + } + return sign_multisig_tx_to_file(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}; @@ -4522,6 +5303,8 @@ void wallet2::transfer_selected(const std::vector<cryptonote::tx_destination_ent // throw if attempting a transaction with no destinations THROW_WALLET_EXCEPTION_IF(dsts.empty(), error::zero_destination); + THROW_WALLET_EXCEPTION_IF(m_multisig, error::wallet_internal_error, "Multisig wallets cannot spend non rct outputs"); + uint64_t upper_transaction_size_limit = get_upper_transaction_size_limit(); uint64_t needed_money = fee; LOG_PRINT_L2("transfer: starting with fee " << print_money (needed_money)); @@ -4594,6 +5377,7 @@ 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; + src.multisig_kLRki = rct::multisig_kLRki({rct::zero(), rct::zero(), rct::zero(), rct::zero()}); detail::print_source_entry(src); ++out_index; } @@ -4621,8 +5405,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); @@ -4691,6 +5476,36 @@ void wallet2::transfer_selected_rct(std::vector<cryptonote::tx_destination_entry THROW_WALLET_EXCEPTION_IF(needed_money < dt.amount, error::tx_sum_overflow, dsts, fee, m_testnet); } + // if this is a multisig wallet, create a list of multisig signers we can use + std::deque<crypto::public_key> multisig_signers; + size_t n_multisig_txes = 0; + if (m_multisig && !m_transfers.empty()) + { + const crypto::public_key local_signer = get_multisig_signer_public_key(); + size_t n_available_signers = 1; + for (const crypto::public_key &signer: m_multisig_signers) + { + if (signer == local_signer) + continue; + multisig_signers.push_front(signer); + for (const auto &i: m_transfers[0].m_multisig_info) + { + if (i.m_signer == signer) + { + multisig_signers.pop_front(); + multisig_signers.push_back(signer); + ++n_available_signers; + break; + } + } + } + multisig_signers.push_back(local_signer); + MDEBUG("We can use " << n_available_signers << "/" << m_multisig_signers.size() << " other signers"); + THROW_WALLET_EXCEPTION_IF(n_available_signers+1 < m_multisig_threshold, error::multisig_import_needed); + n_multisig_txes = n_available_signers == m_multisig_signers.size() ? m_multisig_threshold : 1; + MDEBUG("We will create " << n_multisig_txes << " txes"); + } + uint64_t found_money = 0; for(size_t idx: selected_transfers) { @@ -4711,6 +5526,7 @@ void wallet2::transfer_selected_rct(std::vector<cryptonote::tx_destination_entry LOG_PRINT_L2("preparing outputs"); size_t i = 0, out_index = 0; std::vector<cryptonote::tx_source_entry> sources; + std::unordered_set<rct::key> used_L; for(size_t idx: selected_transfers) { sources.resize(sources.size()+1); @@ -4752,6 +5568,13 @@ 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) + { + crypto::public_key ignore = m_multisig_threshold == m_multisig_signers.size() ? crypto::null_pkey : multisig_signers.front(); + src.multisig_kLRki = get_multisig_composite_kLRki(idx, ignore, used_L, used_L); + } + else + src.multisig_kLRki = rct::multisig_kLRki({rct::zero(), rct::zero(), rct::zero(), rct::zero()}); detail::print_source_entry(src); ++out_index; } @@ -4785,12 +5608,67 @@ 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"); + + std::vector<tools::wallet2::multisig_sig> multisig_sigs; + if (m_multisig) + { + crypto::public_key ignore = m_multisig_threshold == m_multisig_signers.size() ? crypto::null_pkey : multisig_signers.front(); + multisig_sigs.push_back({tx.rct_signatures, ignore, used_L, {}, msout}); + + if (m_multisig_threshold < m_multisig_signers.size()) + { + const crypto::hash prefix_hash = cryptonote::get_transaction_prefix_hash(tx); + + // create the other versions, one for every other participant (the first one's already done above) + for (size_t signer_index = 1; signer_index < n_multisig_txes; ++signer_index) + { + std::unordered_set<rct::key> new_used_L; + size_t src_idx = 0; + THROW_WALLET_EXCEPTION_IF(selected_transfers.size() != sources.size(), error::wallet_internal_error, "mismatched selected_transfers and sources sixes"); + for(size_t idx: selected_transfers) + { + cryptonote::tx_source_entry& src = sources[src_idx]; + src.multisig_kLRki = get_multisig_composite_kLRki(idx, multisig_signers[signer_index], used_L, new_used_L); + ++src_idx; + } + + LOG_PRINT_L2("Creating supplementary multisig transaction"); + cryptonote::transaction ms_tx; + auto sources_copy_copy = sources_copy; + bool r = cryptonote::construct_tx_with_tx_key(m_account.get_keys(), m_subaddresses, sources_copy_copy, splitted_dsts, change_dts.addr, extra, ms_tx, unlock_time,tx_key, additional_tx_keys, true, bulletproof, &msout); + 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); + THROW_WALLET_EXCEPTION_IF(cryptonote::get_transaction_prefix_hash(ms_tx) != prefix_hash, error::wallet_internal_error, "Multisig txes do not share prefix"); + multisig_sigs.push_back({ms_tx.rct_signatures, multisig_signers[signer_index], new_used_L, {}, msout}); + + ms_tx.rct_signatures = tx.rct_signatures; + THROW_WALLET_EXCEPTION_IF(cryptonote::get_transaction_hash(ms_tx) != cryptonote::get_transaction_hash(tx), error::wallet_internal_error, "Multisig txes differ by more than the signatures"); + } + } + } + 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 @@ -4809,13 +5687,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.construction_data.sources = sources; + ptx.multisig_sigs = multisig_sigs; + ptx.construction_data.sources = sources_copy; 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; @@ -4842,13 +5722,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 @@ -5510,7 +6390,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; }; @@ -5886,7 +6766,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) { @@ -6100,6 +6980,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)) @@ -7080,7 +7962,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"); @@ -7195,6 +8077,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) @@ -7479,6 +8362,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)); @@ -7490,6 +8374,287 @@ size_t wallet2::import_outputs(const std::vector<tools::wallet2::transfer_detail return m_transfers.size(); } //---------------------------------------------------------------------------------------------------- +crypto::public_key wallet2::get_multisig_signer_public_key(const crypto::secret_key &spend_skey) const +{ + crypto::public_key pkey; + crypto::secret_key_to_public_key(get_multisig_blinded_secret_key(spend_skey), pkey); + return pkey; +} +//---------------------------------------------------------------------------------------------------- +crypto::public_key wallet2::get_multisig_signer_public_key() const +{ + CHECK_AND_ASSERT_THROW_MES(m_multisig, "Wallet is not multisig"); + crypto::public_key signer; + CHECK_AND_ASSERT_THROW_MES(crypto::secret_key_to_public_key(get_account().get_keys().m_spend_secret_key, signer), "Failed to generate signer public key"); + return signer; +} +//---------------------------------------------------------------------------------------------------- +crypto::public_key wallet2::get_multisig_signing_public_key(const crypto::secret_key &msk) const +{ + CHECK_AND_ASSERT_THROW_MES(m_multisig, "Wallet is not multisig"); + crypto::public_key pkey; + CHECK_AND_ASSERT_THROW_MES(crypto::secret_key_to_public_key(msk, pkey), "Failed to derive public key"); + return pkey; +} +//---------------------------------------------------------------------------------------------------- +crypto::public_key wallet2::get_multisig_signing_public_key(size_t idx) const +{ + CHECK_AND_ASSERT_THROW_MES(m_multisig, "Wallet is not multisig"); + CHECK_AND_ASSERT_THROW_MES(idx < get_account().get_multisig_keys().size(), "Multisig signing key index out of range"); + return get_multisig_signing_public_key(get_account().get_multisig_keys()[idx]); +} +//---------------------------------------------------------------------------------------------------- +rct::key wallet2::get_multisig_k(size_t idx, const std::unordered_set<rct::key> &used_L) const +{ + CHECK_AND_ASSERT_THROW_MES(m_multisig, "Wallet is not multisig"); + CHECK_AND_ASSERT_THROW_MES(idx < m_transfers.size(), "idx out of range"); + for (const auto &k: m_transfers[idx].m_multisig_k) + { + rct::key L; + rct::scalarmultBase(L, k); + if (used_L.find(L) != used_L.end()) + return k; + } + THROW_WALLET_EXCEPTION(tools::error::multisig_export_needed); + return rct::zero(); +} +//---------------------------------------------------------------------------------------------------- +rct::multisig_kLRki wallet2::get_multisig_kLRki(size_t n, const rct::key &k) const +{ + CHECK_AND_ASSERT_THROW_MES(n < m_transfers.size(), "Bad m_transfers index"); + rct::multisig_kLRki kLRki; + kLRki.k = k; + cryptonote::generate_multisig_LR(m_transfers[n].get_public_key(), rct::rct2sk(kLRki.k), (crypto::public_key&)kLRki.L, (crypto::public_key&)kLRki.R); + kLRki.ki = rct::ki2rct(m_transfers[n].m_key_image); + return kLRki; +} +//---------------------------------------------------------------------------------------------------- +rct::multisig_kLRki wallet2::get_multisig_composite_kLRki(size_t n, const crypto::public_key &ignore, std::unordered_set<rct::key> &used_L, std::unordered_set<rct::key> &new_used_L) const +{ + CHECK_AND_ASSERT_THROW_MES(n < m_transfers.size(), "Bad transfer index"); + + const transfer_details &td = m_transfers[n]; + rct::multisig_kLRki kLRki = get_multisig_kLRki(n, rct::skGen()); + + // pick a L/R pair from every other participant but one + size_t n_signers_used = 1; + for (const auto &p: m_transfers[n].m_multisig_info) + { + if (p.m_signer == ignore) + continue; + for (const auto &lr: p.m_LR) + { + if (used_L.find(lr.m_L) != used_L.end()) + continue; + used_L.insert(lr.m_L); + new_used_L.insert(lr.m_L); + rct::addKeys(kLRki.L, kLRki.L, lr.m_L); + rct::addKeys(kLRki.R, kLRki.R, lr.m_R); + ++n_signers_used; + break; + } + } + CHECK_AND_ASSERT_THROW_MES(n_signers_used >= m_multisig_threshold, "LR not found for enough participants"); + + return kLRki; +} +//---------------------------------------------------------------------------------------------------- +crypto::key_image wallet2::get_multisig_composite_key_image(size_t n) const +{ + CHECK_AND_ASSERT_THROW_MES(n < m_transfers.size(), "Bad output index"); + + const transfer_details &td = m_transfers[n]; + const crypto::public_key tx_key = get_tx_pub_key_from_received_outs(td); + const std::vector<crypto::public_key> additional_tx_keys = cryptonote::get_additional_tx_pub_keys_from_extra(td.m_tx); + crypto::key_image ki; + std::vector<crypto::key_image> pkis; + for (const auto &info: td.m_multisig_info) + for (const auto &pki: info.m_partial_key_images) + pkis.push_back(pki); + bool r = cryptonote::generate_multisig_composite_key_image(get_account().get_keys(), m_subaddresses, td.get_public_key(), tx_key, additional_tx_keys, td.m_internal_output_index, pkis, ki); + THROW_WALLET_EXCEPTION_IF(!r, error::wallet_internal_error, "Failed to generate key image"); + return ki; +} +//---------------------------------------------------------------------------------------------------- +cryptonote::blobdata wallet2::export_multisig() +{ + std::vector<tools::wallet2::multisig_info> info; + + const crypto::public_key signer = get_multisig_signer_public_key(); + + info.resize(m_transfers.size()); + for (size_t n = 0; n < m_transfers.size(); ++n) + { + transfer_details &td = m_transfers[n]; + const std::vector<crypto::public_key> additional_tx_pub_keys = get_additional_tx_pub_keys_from_extra(td.m_tx); + crypto::key_image ki; + td.m_multisig_k.clear(); + info[n].m_LR.clear(); + info[n].m_partial_key_images.clear(); + + for (size_t m = 0; m < get_account().get_multisig_keys().size(); ++m) + { + // we want to export the partial key image, not the full one, so we can't use td.m_key_image + bool r = generate_multisig_key_image(get_account().get_keys(), m, td.get_public_key(), ki); + CHECK_AND_ASSERT_THROW_MES(r, "Failed to generate key image"); + info[n].m_partial_key_images.push_back(ki); + } + + size_t nlr = m_multisig_threshold < m_multisig_signers.size() ? m_multisig_threshold - 1 : 1; + for (size_t m = 0; m < nlr; ++m) + { + td.m_multisig_k.push_back(rct::skGen()); + const rct::multisig_kLRki kLRki = get_multisig_kLRki(n, td.m_multisig_k.back()); + info[n].m_LR.push_back({kLRki.L, kLRki.R}); + } + + info[n].m_signer = signer; + } + + std::stringstream oss; + boost::archive::portable_binary_oarchive ar(oss); + ar << info; + + std::string magic(MULTISIG_EXPORT_FILE_MAGIC, strlen(MULTISIG_EXPORT_FILE_MAGIC)); + const cryptonote::account_public_address &keys = get_account().get_keys().m_account_address; + std::string header; + header += std::string((const char *)&keys.m_spend_public_key, sizeof(crypto::public_key)); + header += std::string((const char *)&keys.m_view_public_key, sizeof(crypto::public_key)); + header += std::string((const char *)&signer, sizeof(crypto::public_key)); + std::string ciphertext = encrypt_with_view_secret_key(header + oss.str()); + + return MULTISIG_EXPORT_FILE_MAGIC + ciphertext; +} +//---------------------------------------------------------------------------------------------------- +void wallet2::update_multisig_rescan_info(const std::vector<std::vector<rct::key>> &multisig_k, const std::vector<std::vector<tools::wallet2::multisig_info>> &info, size_t n) +{ + 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 = get_multisig_composite_key_image(n); + 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<cryptonote::blobdata> blobs) +{ + CHECK_AND_ASSERT_THROW_MES(m_multisig, "Wallet is not multisig"); + + std::vector<std::vector<tools::wallet2::multisig_info>> info; + std::unordered_set<crypto::public_key> seen; + for (cryptonote::blobdata &data: blobs) + { + const size_t magiclen = strlen(MULTISIG_EXPORT_FILE_MAGIC); + THROW_WALLET_EXCEPTION_IF(data.size() < magiclen || memcmp(data.data(), MULTISIG_EXPORT_FILE_MAGIC, magiclen), + error::wallet_internal_error, "Bad multisig info file magic in "); + + data = decrypt_with_view_secret_key(std::string(data, magiclen)); + + const size_t headerlen = 3 * sizeof(crypto::public_key); + THROW_WALLET_EXCEPTION_IF(data.size() < headerlen, error::wallet_internal_error, "Bad data size"); + + const crypto::public_key &public_spend_key = *(const crypto::public_key*)&data[0]; + const crypto::public_key &public_view_key = *(const crypto::public_key*)&data[sizeof(crypto::public_key)]; + const crypto::public_key &signer = *(const crypto::public_key*)&data[2*sizeof(crypto::public_key)]; + const cryptonote::account_public_address &keys = get_account().get_keys().m_account_address; + THROW_WALLET_EXCEPTION_IF(public_spend_key != keys.m_spend_public_key || public_view_key != keys.m_view_public_key, + error::wallet_internal_error, "Multisig info is for a different account"); + if (get_multisig_signer_public_key() == signer) + { + MINFO("Multisig info from this wallet ignored"); + continue; + } + if (seen.find(signer) != seen.end()) + { + MINFO("Duplicate multisig info ignored"); + continue; + } + seen.insert(signer); + + std::string body(data, headerlen); + std::istringstream iss(body); + std::vector<tools::wallet2::multisig_info> i; + boost::archive::portable_binary_iarchive ar(iss); + ar >> i; + MINFO(boost::format("%u outputs found") % boost::lexical_cast<std::string>(i.size())); + info.push_back(std::move(i)); + } + + CHECK_AND_ASSERT_THROW_MES(info.size() + 1 <= m_multisig_signers.size() && info.size() + 1 >= m_multisig_threshold, "Wrong number of multisig sources"); + + std::vector<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(); + + if (n_outputs == 0) + return 0; + + // check signers are consistent + for (const auto &pi: info) + { + CHECK_AND_ASSERT_THROW_MES(std::find(m_multisig_signers.begin(), m_multisig_signers.end(), pi[0].m_signer) != m_multisig_signers.end(), + "Signer is not a member of this multisig wallet"); + for (size_t n = 1; n < n_outputs; ++n) + CHECK_AND_ASSERT_THROW_MES(pi[n].m_signer == pi[0].m_signer, "Mismatched signers in imported multisig info"); + } + + // trim data we don't have info for from all participants + for (auto &pi: info) + pi.resize(n_outputs); + + // sort by signer + if (!info.empty() && !info.front().empty()) + { + std::sort(info.begin(), info.end(), [](const std::vector<tools::wallet2::multisig_info> &i0, const std::vector<tools::wallet2::multisig_info> &i1){ return memcmp(&i0[0].m_signer, &i1[0].m_signer, sizeof(i0[0].m_signer)); }); + } + + // 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; + } + + 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 + { + 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 be8315468..399287c3e 100644 --- a/src/wallet/wallet2.h +++ b/src/wallet/wallet2.h @@ -144,10 +144,6 @@ namespace tools RefreshDefault = RefreshOptimizeCoinbase, }; - 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) {} - - public: static const char* tr(const char* str); static bool has_testnet_option(const boost::program_options::variables_map& vm); @@ -166,9 +162,33 @@ namespace tools //! Just parses variables. static std::unique_ptr<wallet2> make_dummy(const boost::program_options::variables_map& vm, const std::function<boost::optional<password_container>(const char *, bool)> &password_prompter); - static bool verify_password(const std::string& keys_file_name, const epee::wipeable_string& password, bool watch_only); + 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); + + struct multisig_info + { + struct LR + { + rct::key m_L; + rct::key m_R; + + BEGIN_SERIALIZE_OBJECT() + FIELD(m_L) + FIELD(m_R) + END_SERIALIZE() + }; + + crypto::public_key m_signer; + std::vector<LR> m_LR; + std::vector<crypto::key_image> m_partial_key_images; // one per key the participant has + + BEGIN_SERIALIZE_OBJECT() + FIELD(m_signer) + FIELD(m_LR) + FIELD(m_partial_key_images) + END_SERIALIZE() + }; struct tx_scan_info_t { @@ -199,6 +219,9 @@ namespace tools bool m_key_image_known; size_t m_pk_index; cryptonote::subaddress_index m_subaddr_index; + bool m_key_image_partial; + std::vector<rct::key> m_multisig_k; + std::vector<multisig_info> m_multisig_info; // one per other participant bool is_rct() const { return m_rct; } uint64_t amount() const { return m_amount; } @@ -219,6 +242,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() }; @@ -308,6 +334,15 @@ namespace tools typedef std::vector<transfer_details> transfer_container; typedef std::unordered_multimap<crypto::hash, payment_details> payment_container; + struct multisig_sig + { + rct::rctSig sigs; + crypto::public_key ignore; + std::unordered_set<rct::key> used_L; + std::unordered_set<crypto::public_key> signing_keys; + rct::multisig_out msout; + }; + // The convention for destinations is: // dests does not include change // splitted_dsts (in construction_data) does @@ -322,6 +357,7 @@ namespace tools crypto::secret_key tx_key; std::vector<crypto::secret_key> additional_tx_keys; std::vector<cryptonote::tx_destination_entry> dests; + std::vector<multisig_sig> multisig_sigs; tx_construction_data construction_data; @@ -337,6 +373,7 @@ namespace tools FIELD(additional_tx_keys) FIELD(dests) FIELD(construction_data) + FIELD(multisig_sigs) END_SERIALIZE() }; @@ -354,6 +391,17 @@ namespace tools std::vector<crypto::key_image> key_images; }; + struct multisig_tx_set + { + std::vector<pending_tx> m_ptx; + std::unordered_set<crypto::public_key> m_signers; + + BEGIN_SERIALIZE_OBJECT() + FIELD(m_ptx) + FIELD(m_signers) + END_SERIALIZE() + }; + struct keys_file_data { crypto::chacha8_iv iv; @@ -419,6 +467,53 @@ namespace tools const cryptonote::account_public_address &account_public_address, const crypto::secret_key& viewkey = crypto::secret_key()); /*! + * \brief Creates a multisig wallet + * \return empty if done, non empty if we need to send another string + * to other participants + */ + std::string make_multisig(const epee::wipeable_string &password, + const std::vector<std::string> &info, + uint32_t threshold); + /*! + * \brief Creates a multisig wallet + * \return empty if done, non empty if we need to send another string + * to other participants + */ + std::string make_multisig(const epee::wipeable_string &password, + const std::vector<crypto::secret_key> &view_keys, + const std::vector<crypto::public_key> &spend_keys, + uint32_t threshold); + /*! + * \brief Finalizes creation of a multisig wallet + */ + bool finalize_multisig(const epee::wipeable_string &password, const std::vector<std::string> &info); + /*! + * \brief Finalizes creation of a multisig wallet + */ + bool finalize_multisig(const epee::wipeable_string &password, std::unordered_set<crypto::public_key> pkeys, std::vector<crypto::public_key> signers); + /*! + * Get a packaged multisig information string + */ + std::string get_multisig_info() const; + /*! + * Verifies and extracts keys from a packaged multisig information string + */ + static bool verify_multisig_info(const std::string &data, crypto::secret_key &skey, crypto::public_key &pkey); + /*! + * Verifies and extracts keys from a packaged multisig information string + */ + static bool verify_extra_multisig_info(const std::string &data, std::unordered_set<crypto::public_key> &pkeys, crypto::public_key &signer); + /*! + * Export multisig info + * This will generate and remember new k values + */ + cryptonote::blobdata 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<cryptonote::blobdata> 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 @@ -497,6 +592,7 @@ namespace tools void expand_subaddresses(const cryptonote::subaddress_index& index); std::string get_subaddress_label(const cryptonote::subaddress_index& index) const; void set_subaddress_label(const cryptonote::subaddress_index &index, const std::string &label); + void set_subaddress_lookahead(size_t major, size_t minor); /*! * \brief Tells if the wallet file is deprecated. */ @@ -512,6 +608,8 @@ namespace tools bool testnet() const { return m_testnet; } bool restricted() const { return m_restricted; } bool watch_only() const { return m_watch_only; } + bool multisig(bool *ready = NULL, 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; @@ -539,6 +637,10 @@ 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); + std::string save_multisig_tx(multisig_tx_set txs); + bool save_multisig_tx(const multisig_tx_set &txs, const std::string &filename); + std::string save_multisig_tx(const std::vector<pending_tx>& ptx_vector); + 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 @@ -551,6 +653,11 @@ 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(cryptonote::blobdata blob, multisig_tx_set &exported_txs, std::function<bool(const multisig_tx_set&)> accept_func = NULL); + 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, std::vector<crypto::hash> &txids); + bool sign_multisig_tx_to_file(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; @@ -724,6 +831,7 @@ namespace tools bool delete_address_book_row(std::size_t row_id); uint64_t get_num_rct_outputs(); + size_t get_num_transfer_details() const { return m_transfers.size(); } const transfer_details &get_transfer_details(size_t idx) const; void get_hard_fork_info(uint8_t version, uint64_t &earliest_height); @@ -829,6 +937,11 @@ namespace tools void set_attribute(const std::string &key, const std::string &value); std::string get_attribute(const std::string &key) const; + crypto::public_key get_multisig_signer_public_key(const crypto::secret_key &spend_skey) const; + crypto::public_key get_multisig_signer_public_key() const; + crypto::public_key get_multisig_signing_public_key(size_t idx) const; + crypto::public_key get_multisig_signing_public_key(const crypto::secret_key &skey) const; + private: /*! * \brief Stores wallet information to wallet file. @@ -864,7 +977,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(); @@ -876,12 +988,16 @@ 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(); + crypto::key_image get_multisig_composite_key_image(size_t n) const; + rct::multisig_kLRki get_multisig_composite_kLRki(size_t n, const crypto::public_key &ignore, std::unordered_set<rct::key> &used_L, std::unordered_set<rct::key> &new_used_L) const; + rct::multisig_kLRki get_multisig_kLRki(size_t n, const rct::key &k) const; + rct::key get_multisig_k(size_t idx, const std::unordered_set<rct::key> &used_L) const; + void update_multisig_rescan_info(const std::vector<std::vector<rct::key>> &multisig_k, const std::vector<std::vector<tools::wallet2::multisig_info>> &info, size_t n); cryptonote::account_base m_account; boost::optional<epee::net_utils::http::login> m_daemon_login; @@ -910,6 +1026,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<std::vector<rct::key>> *m_multisig_rescan_k; std::atomic<bool> m_run; @@ -921,6 +1039,9 @@ namespace tools std::string seed_language; /*!< Language of the mnemonics (seed). */ bool is_old_file_format; /*!< Whether the wallet file is of an old file format */ bool m_watch_only; /*!< no spend key */ + bool m_multisig; /*!< if > 1 spend secret key will not match spend public key */ + uint32_t m_multisig_threshold; + std::vector<crypto::public_key> m_multisig_signers; bool m_always_confirm_transfers; bool m_print_ring_members; bool m_store_tx_info; /*!< request txkey to be returned in RPC, and store in the wallet cache file */ @@ -939,6 +1060,7 @@ namespace tools bool m_is_initialized; NodeRPCProxy m_node_rpc_proxy; std::unordered_set<crypto::hash> m_scanned_pool_txs[2]; + size_t m_subaddress_lookahead_major, m_subaddress_lookahead_minor; // Light wallet bool m_light_wallet; /* sends view key to daemon for scanning */ @@ -956,7 +1078,10 @@ 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_info::LR, 0) +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) @@ -965,7 +1090,8 @@ 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) +BOOST_CLASS_VERSION(tools::wallet2::multisig_sig, 0) namespace boost { @@ -1003,6 +1129,12 @@ namespace boost { x.m_subaddr_index = {}; } + if (ver < 9) + { + x.m_key_image_partial = false; + x.m_multisig_k.clear(); + x.m_multisig_info.clear(); + } } template <class Archive> @@ -1076,6 +1208,36 @@ 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::LR &x, const boost::serialization::version_type ver) + { + a & x.m_L; + a & x.m_R; + } + + template <class Archive> + inline void serialize(Archive &a, tools::wallet2::multisig_info &x, const boost::serialization::version_type ver) + { + a & x.m_signer; + a & x.m_LR; + a & x.m_partial_key_images; + } + + 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> @@ -1254,6 +1416,16 @@ namespace boost } template <class Archive> + inline void serialize(Archive &a, tools::wallet2::multisig_sig &x, const boost::serialization::version_type ver) + { + a & x.sigs; + a & x.ignore; + a & x.used_L; + a & x.signing_keys; + a & x.msout; + } + + template <class Archive> inline void serialize(Archive &a, tools::wallet2::pending_tx &x, const boost::serialization::version_type ver) { a & x.tx; @@ -1281,6 +1453,9 @@ namespace boost if (ver < 2) return; a & x.selected_transfers; + if (ver < 3) + return; + a & x.multisig_sigs; } } } @@ -1356,6 +1531,8 @@ namespace tools // throw if attempting a transaction with no destinations THROW_WALLET_EXCEPTION_IF(dsts.empty(), error::zero_destination); + THROW_WALLET_EXCEPTION_IF(m_multisig, error::wallet_internal_error, "Multisig wallets cannot spend non rct outputs"); + uint64_t upper_transaction_size_limit = get_upper_transaction_size_limit(); uint64_t needed_money = fee; @@ -1458,6 +1635,7 @@ 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; + src.multisig_kLRki = rct::multisig_kLRki({rct::zero(), rct::zero(), rct::zero(), rct::zero()}); detail::print_source_entry(src); ++i; } @@ -1484,7 +1662,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); diff --git a/src/wallet/wallet_args.cpp b/src/wallet/wallet_args.cpp index 91162c4bc..2273f14ad 100644 --- a/src/wallet/wallet_args.cpp +++ b/src/wallet/wallet_args.cpp @@ -85,6 +85,7 @@ namespace wallet_args boost::optional<boost::program_options::variables_map> main( int argc, char** argv, const char* const usage, + const char* const notice, boost::program_options::options_description desc_params, const boost::program_options::positional_options_description& positional_options, const std::function<void(const std::string&, bool)> &print, @@ -179,6 +180,9 @@ namespace wallet_args mlog_set_log(command_line::get_arg(vm, arg_log_level).c_str()); } + if (notice) + Print(print) << notice << ENDL; + if (!command_line::is_arg_defaulted(vm, arg_max_concurrency)) tools::set_max_concurrency(command_line::get_arg(vm, arg_max_concurrency)); diff --git a/src/wallet/wallet_args.h b/src/wallet/wallet_args.h index 8974098ad..212958988 100644 --- a/src/wallet/wallet_args.h +++ b/src/wallet/wallet_args.h @@ -48,6 +48,7 @@ namespace wallet_args boost::optional<boost::program_options::variables_map> main( int argc, char** argv, const char* const usage, + const char* const notice, boost::program_options::options_description desc_params, const boost::program_options::positional_options_description& positional_options, const std::function<void(const std::string&, bool)> &print, 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..0482b9dd6 100644 --- a/src/wallet/wallet_rpc_server.cpp +++ b/src/wallet/wallet_rpc_server.cpp @@ -41,6 +41,7 @@ using namespace epee; #include "common/i18n.h" #include "cryptonote_basic/cryptonote_format_utils.h" #include "cryptonote_basic/account.h" +#include "multisig/multisig.h" #include "wallet_rpc_server_commands_defs.h" #include "misc_language.h" #include "string_coding.h" @@ -324,6 +325,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; @@ -608,11 +610,6 @@ namespace tools return false; } - if (!req.do_not_relay) - m_wallet->commit_tx(ptx_vector); - - // populate response with tx hash - res.tx_hash = epee::string_tools::pod_to_hex(cryptonote::get_transaction_hash(ptx_vector.back().tx)); if (req.get_tx_key) { res.tx_key = epee::string_tools::pod_to_hex(ptx_vector.back().tx_key); @@ -621,18 +618,45 @@ namespace tools } res.fee = ptx_vector.back().fee; - if (req.get_tx_hex) + if (m_wallet->multisig()) { - cryptonote::blobdata blob; - tx_to_blob(ptx_vector.back().tx, blob); - res.tx_blob = epee::string_tools::buff_to_hex_nodelimer(blob); + res.multisig_txset = epee::string_tools::buff_to_hex_nodelimer(m_wallet->save_multisig_tx(ptx_vector)); + if (res.multisig_txset.empty()) + { + er.code = WALLET_RPC_ERROR_CODE_UNKNOWN_ERROR; + er.message = "Failed to save multisig tx set after creation"; + return false; + } } - if (req.get_tx_metadata) + else { - std::ostringstream oss; - binary_archive<true> ar(oss); - ::serialization::serialize(ar, ptx_vector.back()); - res.tx_metadata = epee::string_tools::buff_to_hex_nodelimer(oss.str()); + if (!req.do_not_relay) + m_wallet->commit_tx(ptx_vector); + + // populate response with tx hash + res.tx_hash = epee::string_tools::pod_to_hex(cryptonote::get_transaction_hash(ptx_vector.back().tx)); + if (req.get_tx_hex) + { + cryptonote::blobdata blob; + tx_to_blob(ptx_vector.back().tx, blob); + res.tx_blob = epee::string_tools::buff_to_hex_nodelimer(blob); + } + if (req.get_tx_metadata) + { + std::ostringstream oss; + boost::archive::portable_binary_oarchive ar(oss); + try + { + ar << ptx_vector.back(); + } + catch (...) + { + er.code = WALLET_RPC_ERROR_CODE_UNKNOWN_ERROR; + er.message = "Failed to save multisig tx set after creation"; + return false; + } + res.tx_metadata = epee::string_tools::buff_to_hex_nodelimer(oss.str()); + } } return true; } @@ -673,17 +697,9 @@ namespace tools ptx_vector = m_wallet->create_transactions_2(dsts, mixin, req.unlock_time, req.priority, extra, req.account_index, req.subaddr_indices, m_trusted_daemon); LOG_PRINT_L2("on_transfer_split called create_transactions_2"); - if (!req.do_not_relay) - { - LOG_PRINT_L2("on_transfer_split calling commit_tx"); - m_wallet->commit_tx(ptx_vector); - LOG_PRINT_L2("on_transfer_split called commit_tx"); - } - // populate response with tx hashes for (const auto & ptx : ptx_vector) { - res.tx_hash_list.push_back(epee::string_tools::pod_to_hex(cryptonote::get_transaction_hash(ptx.tx))); if (req.get_tx_keys) { res.tx_key_list.push_back(epee::string_tools::pod_to_hex(ptx.tx_key)); @@ -697,19 +713,56 @@ namespace tools res.amount_list.push_back(ptx_amount); res.fee_list.push_back(ptx.fee); + } - if (req.get_tx_hex) + if (m_wallet->multisig()) + { + res.multisig_txset = epee::string_tools::buff_to_hex_nodelimer(m_wallet->save_multisig_tx(ptx_vector)); + if (res.multisig_txset.empty()) { - cryptonote::blobdata blob; - tx_to_blob(ptx.tx, blob); - res.tx_blob_list.push_back(epee::string_tools::buff_to_hex_nodelimer(blob)); + er.code = WALLET_RPC_ERROR_CODE_UNKNOWN_ERROR; + er.message = "Failed to save multisig tx set after creation"; + return false; } - if (req.get_tx_metadata) + } + + // populate response with tx hashes + for (const auto & ptx : ptx_vector) + { + if (!req.do_not_relay) { - std::ostringstream oss; - binary_archive<true> ar(oss); - ::serialization::serialize(ar, const_cast<tools::wallet2::pending_tx&>(ptx)); - res.tx_metadata_list.push_back(epee::string_tools::buff_to_hex_nodelimer(oss.str())); + LOG_PRINT_L2("on_transfer_split calling commit_tx"); + m_wallet->commit_tx(ptx_vector); + LOG_PRINT_L2("on_transfer_split called commit_tx"); + } + + // populate response with tx hashes + for (auto & ptx : ptx_vector) + { + res.tx_hash_list.push_back(epee::string_tools::pod_to_hex(cryptonote::get_transaction_hash(ptx.tx))); + + if (req.get_tx_hex) + { + cryptonote::blobdata blob; + tx_to_blob(ptx.tx, blob); + res.tx_blob_list.push_back(epee::string_tools::buff_to_hex_nodelimer(blob)); + } + if (req.get_tx_metadata) + { + std::ostringstream oss; + boost::archive::portable_binary_oarchive ar(oss); + try + { + ar << ptx; + } + catch (...) + { + er.code = WALLET_RPC_ERROR_CODE_UNKNOWN_ERROR; + er.message = "Failed to save multisig tx set after creation"; + return false; + } + res.tx_metadata_list.push_back(epee::string_tools::buff_to_hex_nodelimer(oss.str())); + } } } @@ -737,30 +790,65 @@ namespace tools { std::vector<wallet2::pending_tx> ptx_vector = m_wallet->create_unmixable_sweep_transactions(m_trusted_daemon); - if (!req.do_not_relay) - m_wallet->commit_tx(ptx_vector); - - // populate response with tx hashes for (const auto & ptx : ptx_vector) { - res.tx_hash_list.push_back(epee::string_tools::pod_to_hex(cryptonote::get_transaction_hash(ptx.tx))); if (req.get_tx_keys) { res.tx_key_list.push_back(epee::string_tools::pod_to_hex(ptx.tx_key)); } res.fee_list.push_back(ptx.fee); - if (req.get_tx_hex) + } + + if (m_wallet->multisig()) + { + for (tools::wallet2::pending_tx &ptx: ptx_vector) { - cryptonote::blobdata blob; - tx_to_blob(ptx.tx, blob); - res.tx_blob_list.push_back(epee::string_tools::buff_to_hex_nodelimer(blob)); + std::ostringstream oss; + boost::archive::portable_binary_oarchive ar(oss); + try + { + ar << ptx; + } + catch (...) + { + er.code = WALLET_RPC_ERROR_CODE_UNKNOWN_ERROR; + er.message = "Failed to save multisig tx set after creation"; + return false; + } + res.multisig_txset.push_back(epee::string_tools::buff_to_hex_nodelimer(oss.str())); } - if (req.get_tx_metadata) + } + else + { + if (!req.do_not_relay) + m_wallet->commit_tx(ptx_vector); + + // populate response with tx hashes + for (auto & ptx : ptx_vector) { - std::ostringstream oss; - binary_archive<true> ar(oss); - ::serialization::serialize(ar, const_cast<tools::wallet2::pending_tx&>(ptx)); - res.tx_metadata_list.push_back(epee::string_tools::buff_to_hex_nodelimer(oss.str())); + res.tx_hash_list.push_back(epee::string_tools::pod_to_hex(cryptonote::get_transaction_hash(ptx.tx))); + if (req.get_tx_hex) + { + cryptonote::blobdata blob; + tx_to_blob(ptx.tx, blob); + res.tx_blob_list.push_back(epee::string_tools::buff_to_hex_nodelimer(blob)); + } + if (req.get_tx_metadata) + { + std::ostringstream oss; + boost::archive::portable_binary_oarchive ar(oss); + try + { + ar << ptx; + } + catch (...) + { + er.code = WALLET_RPC_ERROR_CODE_UNKNOWN_ERROR; + er.message = "Failed to save multisig tx set after creation"; + return false; + } + res.tx_metadata_list.push_back(epee::string_tools::buff_to_hex_nodelimer(oss.str())); + } } } @@ -802,29 +890,64 @@ namespace tools uint64_t mixin = m_wallet->adjust_mixin(req.mixin); std::vector<wallet2::pending_tx> ptx_vector = m_wallet->create_transactions_all(req.below_amount, dsts[0].addr, dsts[0].is_subaddress, mixin, req.unlock_time, req.priority, extra, req.account_index, req.subaddr_indices, m_trusted_daemon); - if (!req.do_not_relay) - m_wallet->commit_tx(ptx_vector); - - // populate response with tx hashes for (const auto & ptx : ptx_vector) { - res.tx_hash_list.push_back(epee::string_tools::pod_to_hex(cryptonote::get_transaction_hash(ptx.tx))); if (req.get_tx_keys) { res.tx_key_list.push_back(epee::string_tools::pod_to_hex(ptx.tx_key)); } - if (req.get_tx_hex) + } + + if (m_wallet->multisig()) + { + for (tools::wallet2::pending_tx &ptx: ptx_vector) { - cryptonote::blobdata blob; - tx_to_blob(ptx.tx, blob); - res.tx_blob_list.push_back(epee::string_tools::buff_to_hex_nodelimer(blob)); + std::ostringstream oss; + boost::archive::portable_binary_oarchive ar(oss); + try + { + ar << ptx; + } + catch (...) + { + er.code = WALLET_RPC_ERROR_CODE_UNKNOWN_ERROR; + er.message = "Failed to save multisig tx set after creation"; + return false; + } + res.multisig_txset.push_back(epee::string_tools::buff_to_hex_nodelimer(oss.str())); } - if (req.get_tx_metadata) + } + else + { + if (!req.do_not_relay) + m_wallet->commit_tx(ptx_vector); + + // populate response with tx hashes + for (auto & ptx : ptx_vector) { - std::ostringstream oss; - binary_archive<true> ar(oss); - ::serialization::serialize(ar, const_cast<tools::wallet2::pending_tx&>(ptx)); - res.tx_metadata_list.push_back(epee::string_tools::buff_to_hex_nodelimer(oss.str())); + res.tx_hash_list.push_back(epee::string_tools::pod_to_hex(cryptonote::get_transaction_hash(ptx.tx))); + if (req.get_tx_hex) + { + cryptonote::blobdata blob; + tx_to_blob(ptx.tx, blob); + res.tx_blob_list.push_back(epee::string_tools::buff_to_hex_nodelimer(blob)); + } + if (req.get_tx_metadata) + { + std::ostringstream oss; + boost::archive::portable_binary_oarchive ar(oss); + try + { + ar << ptx; + } + catch (...) + { + er.code = WALLET_RPC_ERROR_CODE_UNKNOWN_ERROR; + er.message = "Failed to save multisig tx set after creation"; + return false; + } + res.tx_metadata_list.push_back(epee::string_tools::buff_to_hex_nodelimer(oss.str())); + } } } @@ -886,37 +1009,59 @@ namespace tools er.message = "Multiple transactions are created, which is not supposed to happen"; return false; } - if (ptx_vector[0].selected_transfers.size() > 1) + const wallet2::pending_tx &ptx = ptx_vector[0]; + if (ptx.selected_transfers.size() > 1) { er.code = WALLET_RPC_ERROR_CODE_UNKNOWN_ERROR; er.message = "The transaction uses multiple inputs, which is not supposed to happen"; return false; } - if (!req.do_not_relay) - m_wallet->commit_tx(ptx_vector); - - // populate response with tx hashes - const wallet2::pending_tx &ptx = ptx_vector[0]; - res.tx_hash = epee::string_tools::pod_to_hex(cryptonote::get_transaction_hash(ptx.tx)); if (req.get_tx_key) { res.tx_key = epee::string_tools::pod_to_hex(ptx.tx_key); } - if (req.get_tx_hex) + + if (m_wallet->multisig()) { - cryptonote::blobdata blob; - tx_to_blob(ptx.tx, blob); - res.tx_blob = epee::string_tools::buff_to_hex_nodelimer(blob); + res.multisig_txset = epee::string_tools::buff_to_hex_nodelimer(m_wallet->save_multisig_tx(ptx_vector)); + if (res.multisig_txset.empty()) + { + er.code = WALLET_RPC_ERROR_CODE_UNKNOWN_ERROR; + er.message = "Failed to save multisig tx set after creation"; + return false; + } } - if (req.get_tx_metadata) + else { - std::ostringstream oss; - binary_archive<true> ar(oss); - ::serialization::serialize(ar, const_cast<tools::wallet2::pending_tx&>(ptx)); - res.tx_metadata = epee::string_tools::buff_to_hex_nodelimer(oss.str()); - } + if (!req.do_not_relay) + m_wallet->commit_tx(ptx_vector); + // populate response with tx hashes + res.tx_hash = epee::string_tools::pod_to_hex(cryptonote::get_transaction_hash(ptx.tx)); + if (req.get_tx_hex) + { + cryptonote::blobdata blob; + tx_to_blob(ptx.tx, blob); + res.tx_blob = epee::string_tools::buff_to_hex_nodelimer(blob); + } + if (req.get_tx_metadata) + { + std::ostringstream oss; + boost::archive::portable_binary_oarchive ar(oss); + try + { + ar << ptx; + } + catch (...) + { + er.code = WALLET_RPC_ERROR_CODE_UNKNOWN_ERROR; + er.message = "Failed to save multisig tx set after creation"; + return false; + } + res.tx_metadata = epee::string_tools::buff_to_hex_nodelimer(oss.str()); + } + } return true; } catch (const tools::error::daemon_busy& e) @@ -952,13 +1097,14 @@ namespace tools return false; } - std::stringstream ss; - ss << blob; - binary_archive<false> ba(ss); - tools::wallet2::pending_tx ptx; - bool r = ::serialization::serialize(ba, ptx); - if (!r) + try + { + std::istringstream iss(blob); + boost::archive::portable_binary_iarchive ar(iss); + ar >> ptx; + } + catch (...) { er.code = WALLET_RPC_ERROR_CODE_BAD_TX_METADATA; er.message = "Failed to parse tx metadata."; @@ -2338,6 +2484,375 @@ namespace tools } } //------------------------------------------------------------------------------------------------------------------------------ + bool wallet_rpc_server::on_is_multisig(const wallet_rpc::COMMAND_RPC_IS_MULTISIG::request& req, wallet_rpc::COMMAND_RPC_IS_MULTISIG::response& res, epee::json_rpc::error& er) + { + if (!m_wallet) return not_open(er); + res.multisig = m_wallet->multisig(&res.ready, &res.threshold, &res.total); + return true; + } + //------------------------------------------------------------------------------------------------------------------------------ + bool wallet_rpc_server::on_prepare_multisig(const wallet_rpc::COMMAND_RPC_PREPARE_MULTISIG::request& req, wallet_rpc::COMMAND_RPC_PREPARE_MULTISIG::response& res, epee::json_rpc::error& er) + { + if (!m_wallet) return not_open(er); + if (m_wallet->restricted()) + { + er.code = WALLET_RPC_ERROR_CODE_DENIED; + er.message = "Command unavailable in restricted mode."; + return false; + } + if (m_wallet->multisig()) + { + er.code = WALLET_RPC_ERROR_CODE_ALREADY_MULTISIG; + er.message = "This wallet is already multisig"; + return false; + } + if (m_wallet->watch_only()) + { + er.code = WALLET_RPC_ERROR_CODE_WATCH_ONLY; + er.message = "wallet is watch-only and cannot be made multisig"; + return false; + } + + res.multisig_info = m_wallet->get_multisig_info(); + return true; + } + //------------------------------------------------------------------------------------------------------------------------------ + bool wallet_rpc_server::on_make_multisig(const wallet_rpc::COMMAND_RPC_MAKE_MULTISIG::request& req, wallet_rpc::COMMAND_RPC_MAKE_MULTISIG::response& res, epee::json_rpc::error& er) + { + if (!m_wallet) return not_open(er); + if (m_wallet->restricted()) + { + er.code = WALLET_RPC_ERROR_CODE_DENIED; + er.message = "Command unavailable in restricted mode."; + return false; + } + if (m_wallet->multisig()) + { + er.code = WALLET_RPC_ERROR_CODE_ALREADY_MULTISIG; + er.message = "This wallet is already multisig"; + return false; + } + if (m_wallet->watch_only()) + { + er.code = WALLET_RPC_ERROR_CODE_WATCH_ONLY; + er.message = "wallet is watch-only and cannot be made multisig"; + return false; + } + + try + { + res.multisig_info = m_wallet->make_multisig(req.password, req.multisig_info, req.threshold); + res.address = m_wallet->get_account().get_public_address_str(m_wallet->testnet()); + } + catch (const std::exception &e) + { + er.code = WALLET_RPC_ERROR_CODE_UNKNOWN_ERROR; + er.message = e.what(); + return false; + } + + return true; + } + //------------------------------------------------------------------------------------------------------------------------------ + bool wallet_rpc_server::on_export_multisig(const wallet_rpc::COMMAND_RPC_EXPORT_MULTISIG::request& req, wallet_rpc::COMMAND_RPC_EXPORT_MULTISIG::response& res, epee::json_rpc::error& er) + { + if (!m_wallet) return not_open(er); + if (m_wallet->restricted()) + { + er.code = WALLET_RPC_ERROR_CODE_DENIED; + er.message = "Command unavailable in restricted mode."; + return false; + } + bool ready; + if (!m_wallet->multisig(&ready)) + { + er.code = WALLET_RPC_ERROR_CODE_NOT_MULTISIG; + er.message = "This wallet is not multisig"; + return false; + } + if (!ready) + { + er.code = WALLET_RPC_ERROR_CODE_NOT_MULTISIG; + er.message = "This wallet is multisig, but not yet finalized"; + return false; + } + + cryptonote::blobdata info; + try + { + info = m_wallet->export_multisig(); + } + catch (const std::exception &e) + { + er.code = WALLET_RPC_ERROR_CODE_UNKNOWN_ERROR; + er.message = e.what(); + return false; + } + + res.info = epee::string_tools::buff_to_hex_nodelimer(info); + + return true; + } + //------------------------------------------------------------------------------------------------------------------------------ + bool wallet_rpc_server::on_import_multisig(const wallet_rpc::COMMAND_RPC_IMPORT_MULTISIG::request& req, wallet_rpc::COMMAND_RPC_IMPORT_MULTISIG::response& res, epee::json_rpc::error& er) + { + if (!m_wallet) return not_open(er); + if (m_wallet->restricted()) + { + er.code = WALLET_RPC_ERROR_CODE_DENIED; + er.message = "Command unavailable in restricted mode."; + return false; + } + bool ready; + uint32_t threshold, total; + if (!m_wallet->multisig(&ready, &threshold, &total)) + { + er.code = WALLET_RPC_ERROR_CODE_NOT_MULTISIG; + er.message = "This wallet is not multisig"; + return false; + } + if (!ready) + { + er.code = WALLET_RPC_ERROR_CODE_NOT_MULTISIG; + er.message = "This wallet is multisig, but not yet finalized"; + return false; + } + + if (req.info.size() < threshold - 1) + { + er.code = WALLET_RPC_ERROR_CODE_THRESHOLD_NOT_REACHED; + er.message = "Needs multisig export info from more participants"; + return false; + } + + std::vector<cryptonote::blobdata> info; + info.resize(req.info.size()); + for (size_t n = 0; n < info.size(); ++n) + { + if (!epee::string_tools::parse_hexstr_to_binbuff(req.info[n], info[n])) + { + er.code = WALLET_RPC_ERROR_CODE_BAD_HEX; + er.message = "Failed to parse hex."; + return false; + } + } + + try + { + res.n_outputs = m_wallet->import_multisig(info); + } + catch (const std::exception &e) + { + er.code = WALLET_RPC_ERROR_CODE_UNKNOWN_ERROR; + er.message = "Error calling import_multisig"; + return false; + } + + if (m_trusted_daemon) + { + try + { + m_wallet->rescan_spent(); + } + catch (const std::exception &e) + { + er.message = std::string("Success, but failed to update spent status after import multisig info: ") + e.what(); + } + } + else + { + er.message = "Success, but cannot update spent status after import multisig info as dameon is untrusted"; + } + + return true; + } + //------------------------------------------------------------------------------------------------------------------------------ + bool wallet_rpc_server::on_finalize_multisig(const wallet_rpc::COMMAND_RPC_FINALIZE_MULTISIG::request& req, wallet_rpc::COMMAND_RPC_FINALIZE_MULTISIG::response& res, epee::json_rpc::error& er) + { + if (!m_wallet) return not_open(er); + if (m_wallet->restricted()) + { + er.code = WALLET_RPC_ERROR_CODE_DENIED; + er.message = "Command unavailable in restricted mode."; + return false; + } + bool ready; + uint32_t threshold, total; + if (!m_wallet->multisig(&ready, &threshold, &total)) + { + er.code = WALLET_RPC_ERROR_CODE_NOT_MULTISIG; + er.message = "This wallet is not multisig"; + return false; + } + if (ready) + { + er.code = WALLET_RPC_ERROR_CODE_ALREADY_MULTISIG; + er.message = "This wallet is multisig, and already finalized"; + return false; + } + + if (req.multisig_info.size() < threshold - 1) + { + er.code = WALLET_RPC_ERROR_CODE_THRESHOLD_NOT_REACHED; + er.message = "Needs multisig info from more participants"; + return false; + } + + try + { + if (!m_wallet->finalize_multisig(req.password, req.multisig_info)) + { + er.code = WALLET_RPC_ERROR_CODE_UNKNOWN_ERROR; + er.message = "Error calling finalize_multisig"; + return false; + } + } + catch (const std::exception &e) + { + er.code = WALLET_RPC_ERROR_CODE_UNKNOWN_ERROR; + er.message = std::string("Error calling finalize_multisig: ") + e.what(); + return false; + } + res.address = m_wallet->get_account().get_public_address_str(m_wallet->testnet()); + + return true; + } + //------------------------------------------------------------------------------------------------------------------------------ + bool wallet_rpc_server::on_sign_multisig(const wallet_rpc::COMMAND_RPC_SIGN_MULTISIG::request& req, wallet_rpc::COMMAND_RPC_SIGN_MULTISIG::response& res, epee::json_rpc::error& er) + { + if (!m_wallet) return not_open(er); + if (m_wallet->restricted()) + { + er.code = WALLET_RPC_ERROR_CODE_DENIED; + er.message = "Command unavailable in restricted mode."; + return false; + } + bool ready; + uint32_t threshold, total; + if (!m_wallet->multisig(&ready, &threshold, &total)) + { + er.code = WALLET_RPC_ERROR_CODE_NOT_MULTISIG; + er.message = "This wallet is not multisig"; + return false; + } + if (!ready) + { + er.code = WALLET_RPC_ERROR_CODE_NOT_MULTISIG; + er.message = "This wallet is multisig, but not yet finalized"; + return false; + } + + cryptonote::blobdata blob; + if (!epee::string_tools::parse_hexstr_to_binbuff(req.tx_data_hex, blob)) + { + er.code = WALLET_RPC_ERROR_CODE_BAD_HEX; + er.message = "Failed to parse hex."; + return false; + } + + tools::wallet2::multisig_tx_set txs; + bool r = m_wallet->load_multisig_tx(blob, txs, NULL); + if (!r) + { + er.code = WALLET_RPC_ERROR_CODE_BAD_MULTISIG_TX_DATA; + er.message = "Failed to parse multisig tx data."; + return false; + } + + std::vector<crypto::hash> txids; + try + { + bool r = m_wallet->sign_multisig_tx(txs, txids); + if (!r) + { + er.code = WALLET_RPC_ERROR_CODE_MULTISIG_SIGNATURE; + er.message = "Failed to sign multisig tx"; + return false; + } + } + catch (const std::exception &e) + { + er.code = WALLET_RPC_ERROR_CODE_MULTISIG_SIGNATURE; + er.message = std::string("Failed to sign multisig tx: ") + e.what(); + return false; + } + + res.tx_data_hex = epee::string_tools::buff_to_hex_nodelimer(m_wallet->save_multisig_tx(txs)); + if (!txids.empty()) + { + for (const crypto::hash &txid: txids) + res.tx_hash_list.push_back(epee::string_tools::pod_to_hex(txid)); + } + + return true; + } + //------------------------------------------------------------------------------------------------------------------------------ + bool wallet_rpc_server::on_submit_multisig(const wallet_rpc::COMMAND_RPC_SUBMIT_MULTISIG::request& req, wallet_rpc::COMMAND_RPC_SUBMIT_MULTISIG::response& res, epee::json_rpc::error& er) + { + if (!m_wallet) return not_open(er); + if (m_wallet->restricted()) + { + er.code = WALLET_RPC_ERROR_CODE_DENIED; + er.message = "Command unavailable in restricted mode."; + return false; + } + bool ready; + uint32_t threshold, total; + if (!m_wallet->multisig(&ready, &threshold, &total)) + { + er.code = WALLET_RPC_ERROR_CODE_NOT_MULTISIG; + er.message = "This wallet is not multisig"; + return false; + } + if (!ready) + { + er.code = WALLET_RPC_ERROR_CODE_NOT_MULTISIG; + er.message = "This wallet is multisig, but not yet finalized"; + return false; + } + + cryptonote::blobdata blob; + if (!epee::string_tools::parse_hexstr_to_binbuff(req.tx_data_hex, blob)) + { + er.code = WALLET_RPC_ERROR_CODE_BAD_HEX; + er.message = "Failed to parse hex."; + return false; + } + + tools::wallet2::multisig_tx_set txs; + bool r = m_wallet->load_multisig_tx(blob, txs, NULL); + if (!r) + { + er.code = WALLET_RPC_ERROR_CODE_BAD_MULTISIG_TX_DATA; + er.message = "Failed to parse multisig tx data."; + return false; + } + + if (txs.m_signers.size() < threshold) + { + er.code = WALLET_RPC_ERROR_CODE_THRESHOLD_NOT_REACHED; + er.message = "Not enough signers signed this transaction."; + return false; + } + + try + { + for (auto &ptx: txs.m_ptx) + { + m_wallet->commit_tx(ptx); + res.tx_hash_list.push_back(epee::string_tools::pod_to_hex(cryptonote::get_transaction_hash(ptx.tx))); + } + } + catch (const std::exception &e) + { + er.code = WALLET_RPC_ERROR_CODE_MULTISIG_SUBMISSION; + er.message = std::string("Failed to submit multisig tx: ") + e.what(); + return false; + } + + return true; + } + //------------------------------------------------------------------------------------------------------------------------------ } int main(int argc, char** argv) { @@ -2360,6 +2875,7 @@ int main(int argc, char** argv) { const auto vm = wallet_args::main( argc, argv, "monero-wallet-rpc [--wallet-file=<file>|--generate-from-json=<file>|--wallet-dir=<directory>] [--rpc-bind-port=<port>]", + tools::wallet_rpc_server::tr("This is the RPC monero wallet. It needs to connect to a monero\ndaemon to work correctly."), desc_params, po::positional_options_description(), [](const std::string &s, bool emphasis){ epee::set_console_color(emphasis ? epee::console_color_white : epee::console_color_default, true); std::cout << s << std::endl; if (emphasis) epee::reset_console_color(); }, diff --git a/src/wallet/wallet_rpc_server.h b/src/wallet/wallet_rpc_server.h index 9455c4769..79f589623 100644 --- a/src/wallet/wallet_rpc_server.h +++ b/src/wallet/wallet_rpc_server.h @@ -117,6 +117,14 @@ namespace tools MAP_JON_RPC_WE("get_languages", on_get_languages, wallet_rpc::COMMAND_RPC_GET_LANGUAGES) MAP_JON_RPC_WE("create_wallet", on_create_wallet, wallet_rpc::COMMAND_RPC_CREATE_WALLET) MAP_JON_RPC_WE("open_wallet", on_open_wallet, wallet_rpc::COMMAND_RPC_OPEN_WALLET) + MAP_JON_RPC_WE("is_multisig", on_is_multisig, wallet_rpc::COMMAND_RPC_IS_MULTISIG) + MAP_JON_RPC_WE("prepare_multisig", on_prepare_multisig, wallet_rpc::COMMAND_RPC_PREPARE_MULTISIG) + MAP_JON_RPC_WE("make_multisig", on_make_multisig, wallet_rpc::COMMAND_RPC_MAKE_MULTISIG) + MAP_JON_RPC_WE("export_multisig_info", on_export_multisig, wallet_rpc::COMMAND_RPC_EXPORT_MULTISIG) + MAP_JON_RPC_WE("import_multisig_info", on_import_multisig, wallet_rpc::COMMAND_RPC_IMPORT_MULTISIG) + MAP_JON_RPC_WE("finalize_multisig", on_finalize_multisig, wallet_rpc::COMMAND_RPC_FINALIZE_MULTISIG) + MAP_JON_RPC_WE("sign_multisig", on_sign_multisig, wallet_rpc::COMMAND_RPC_SIGN_MULTISIG) + MAP_JON_RPC_WE("submit_multisig", on_submit_multisig, wallet_rpc::COMMAND_RPC_SUBMIT_MULTISIG) END_JSON_RPC_MAP() END_URI_MAP2() @@ -171,6 +179,14 @@ namespace tools bool on_get_languages(const wallet_rpc::COMMAND_RPC_GET_LANGUAGES::request& req, wallet_rpc::COMMAND_RPC_GET_LANGUAGES::response& res, epee::json_rpc::error& er); bool on_create_wallet(const wallet_rpc::COMMAND_RPC_CREATE_WALLET::request& req, wallet_rpc::COMMAND_RPC_CREATE_WALLET::response& res, epee::json_rpc::error& er); bool on_open_wallet(const wallet_rpc::COMMAND_RPC_OPEN_WALLET::request& req, wallet_rpc::COMMAND_RPC_OPEN_WALLET::response& res, epee::json_rpc::error& er); + bool on_is_multisig(const wallet_rpc::COMMAND_RPC_IS_MULTISIG::request& req, wallet_rpc::COMMAND_RPC_IS_MULTISIG::response& res, epee::json_rpc::error& er); + bool on_prepare_multisig(const wallet_rpc::COMMAND_RPC_PREPARE_MULTISIG::request& req, wallet_rpc::COMMAND_RPC_PREPARE_MULTISIG::response& res, epee::json_rpc::error& er); + bool on_make_multisig(const wallet_rpc::COMMAND_RPC_MAKE_MULTISIG::request& req, wallet_rpc::COMMAND_RPC_MAKE_MULTISIG::response& res, epee::json_rpc::error& er); + bool on_export_multisig(const wallet_rpc::COMMAND_RPC_EXPORT_MULTISIG::request& req, wallet_rpc::COMMAND_RPC_EXPORT_MULTISIG::response& res, epee::json_rpc::error& er); + bool on_import_multisig(const wallet_rpc::COMMAND_RPC_IMPORT_MULTISIG::request& req, wallet_rpc::COMMAND_RPC_IMPORT_MULTISIG::response& res, epee::json_rpc::error& er); + bool on_finalize_multisig(const wallet_rpc::COMMAND_RPC_FINALIZE_MULTISIG::request& req, wallet_rpc::COMMAND_RPC_FINALIZE_MULTISIG::response& res, epee::json_rpc::error& er); + bool on_sign_multisig(const wallet_rpc::COMMAND_RPC_SIGN_MULTISIG::request& req, wallet_rpc::COMMAND_RPC_SIGN_MULTISIG::response& res, epee::json_rpc::error& er); + bool on_submit_multisig(const wallet_rpc::COMMAND_RPC_SUBMIT_MULTISIG::request& req, wallet_rpc::COMMAND_RPC_SUBMIT_MULTISIG::response& res, epee::json_rpc::error& er); //json rpc v2 bool on_query_key(const wallet_rpc::COMMAND_RPC_QUERY_KEY::request& req, wallet_rpc::COMMAND_RPC_QUERY_KEY::response& res, epee::json_rpc::error& er); diff --git a/src/wallet/wallet_rpc_server_commands_defs.h b/src/wallet/wallet_rpc_server_commands_defs.h index e084d9e6d..57cc01e27 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() }; @@ -316,6 +318,7 @@ namespace wallet_rpc uint64_t fee; std::string tx_blob; std::string tx_metadata; + std::string multisig_txset; BEGIN_KV_SERIALIZE_MAP() KV_SERIALIZE(tx_hash) @@ -324,6 +327,7 @@ namespace wallet_rpc KV_SERIALIZE(fee) KV_SERIALIZE(tx_blob) KV_SERIALIZE(tx_metadata) + KV_SERIALIZE(multisig_txset) END_KV_SERIALIZE_MAP() }; }; @@ -376,6 +380,7 @@ namespace wallet_rpc std::list<uint64_t> fee_list; std::list<std::string> tx_blob_list; std::list<std::string> tx_metadata_list; + std::string multisig_txset; BEGIN_KV_SERIALIZE_MAP() KV_SERIALIZE(tx_hash_list) @@ -384,6 +389,7 @@ namespace wallet_rpc KV_SERIALIZE(fee_list) KV_SERIALIZE(tx_blob_list) KV_SERIALIZE(tx_metadata_list) + KV_SERIALIZE(multisig_txset) END_KV_SERIALIZE_MAP() }; }; @@ -421,6 +427,7 @@ namespace wallet_rpc std::list<uint64_t> fee_list; std::list<std::string> tx_blob_list; std::list<std::string> tx_metadata_list; + std::list<std::string> multisig_txset; BEGIN_KV_SERIALIZE_MAP() KV_SERIALIZE(tx_hash_list) @@ -428,6 +435,7 @@ namespace wallet_rpc KV_SERIALIZE(fee_list) KV_SERIALIZE(tx_blob_list) KV_SERIALIZE(tx_metadata_list) + KV_SERIALIZE(multisig_txset) END_KV_SERIALIZE_MAP() }; }; @@ -481,6 +489,7 @@ namespace wallet_rpc std::list<uint64_t> fee_list; std::list<std::string> tx_blob_list; std::list<std::string> tx_metadata_list; + std::list<std::string> multisig_txset; BEGIN_KV_SERIALIZE_MAP() KV_SERIALIZE(tx_hash_list) @@ -488,6 +497,7 @@ namespace wallet_rpc KV_SERIALIZE(fee_list) KV_SERIALIZE(tx_blob_list) KV_SERIALIZE(tx_metadata_list) + KV_SERIALIZE(multisig_txset) END_KV_SERIALIZE_MAP() }; }; @@ -528,6 +538,7 @@ namespace wallet_rpc uint64_t fee; std::string tx_blob; std::string tx_metadata; + std::string multisig_txset; BEGIN_KV_SERIALIZE_MAP() KV_SERIALIZE(tx_hash) @@ -535,6 +546,7 @@ namespace wallet_rpc KV_SERIALIZE(fee) KV_SERIALIZE(tx_blob) KV_SERIALIZE(tx_metadata) + KV_SERIALIZE(multisig_txset) END_KV_SERIALIZE_MAP() }; }; @@ -1485,5 +1497,181 @@ namespace wallet_rpc END_KV_SERIALIZE_MAP() }; }; + + struct COMMAND_RPC_IS_MULTISIG + { + struct request + { + BEGIN_KV_SERIALIZE_MAP() + END_KV_SERIALIZE_MAP() + }; + + struct response + { + bool multisig; + bool ready; + uint32_t threshold; + uint32_t total; + + BEGIN_KV_SERIALIZE_MAP() + KV_SERIALIZE(multisig) + KV_SERIALIZE(ready) + KV_SERIALIZE(threshold) + KV_SERIALIZE(total) + END_KV_SERIALIZE_MAP() + }; + }; + + struct COMMAND_RPC_PREPARE_MULTISIG + { + struct request + { + BEGIN_KV_SERIALIZE_MAP() + END_KV_SERIALIZE_MAP() + }; + + struct response + { + std::string multisig_info; + + BEGIN_KV_SERIALIZE_MAP() + KV_SERIALIZE(multisig_info) + END_KV_SERIALIZE_MAP() + }; + }; + + struct COMMAND_RPC_MAKE_MULTISIG + { + struct request + { + std::vector<std::string> multisig_info; + uint32_t threshold; + std::string password; + + BEGIN_KV_SERIALIZE_MAP() + KV_SERIALIZE(multisig_info) + KV_SERIALIZE(threshold) + KV_SERIALIZE(password) + END_KV_SERIALIZE_MAP() + }; + + struct response + { + std::string address; + std::string multisig_info; + + BEGIN_KV_SERIALIZE_MAP() + KV_SERIALIZE(address) + KV_SERIALIZE(multisig_info) + END_KV_SERIALIZE_MAP() + }; + }; + + struct COMMAND_RPC_EXPORT_MULTISIG + { + struct request + { + BEGIN_KV_SERIALIZE_MAP() + END_KV_SERIALIZE_MAP() + }; + + struct response + { + std::string info; + + BEGIN_KV_SERIALIZE_MAP() + KV_SERIALIZE(info) + END_KV_SERIALIZE_MAP() + }; + }; + + struct COMMAND_RPC_IMPORT_MULTISIG + { + struct request + { + std::vector<std::string> info; + + BEGIN_KV_SERIALIZE_MAP() + KV_SERIALIZE(info) + END_KV_SERIALIZE_MAP() + }; + + struct response + { + uint64_t n_outputs; + + BEGIN_KV_SERIALIZE_MAP() + KV_SERIALIZE(n_outputs) + END_KV_SERIALIZE_MAP() + }; + }; + + struct COMMAND_RPC_FINALIZE_MULTISIG + { + struct request + { + std::string password; + std::vector<std::string> multisig_info; + + BEGIN_KV_SERIALIZE_MAP() + KV_SERIALIZE(password) + KV_SERIALIZE(multisig_info) + END_KV_SERIALIZE_MAP() + }; + + struct response + { + std::string address; + + BEGIN_KV_SERIALIZE_MAP() + KV_SERIALIZE(address) + END_KV_SERIALIZE_MAP() + }; + }; + + struct COMMAND_RPC_SIGN_MULTISIG + { + struct request + { + std::string tx_data_hex; + + BEGIN_KV_SERIALIZE_MAP() + KV_SERIALIZE(tx_data_hex) + END_KV_SERIALIZE_MAP() + }; + + struct response + { + std::string tx_data_hex; + std::list<std::string> tx_hash_list; + + BEGIN_KV_SERIALIZE_MAP() + KV_SERIALIZE(tx_data_hex) + KV_SERIALIZE(tx_hash_list) + END_KV_SERIALIZE_MAP() + }; + }; + + struct COMMAND_RPC_SUBMIT_MULTISIG + { + struct request + { + std::string tx_data_hex; + + BEGIN_KV_SERIALIZE_MAP() + KV_SERIALIZE(tx_data_hex) + END_KV_SERIALIZE_MAP() + }; + + struct response + { + std::list<std::string> tx_hash_list; + + BEGIN_KV_SERIALIZE_MAP() + KV_SERIALIZE(tx_hash_list) + END_KV_SERIALIZE_MAP() + }; + }; + } } diff --git a/src/wallet/wallet_rpc_server_error_codes.h b/src/wallet/wallet_rpc_server_error_codes.h index c3f3e20d1..578413e38 100644 --- a/src/wallet/wallet_rpc_server_error_codes.h +++ b/src/wallet/wallet_rpc_server_error_codes.h @@ -58,3 +58,12 @@ #define WALLET_RPC_ERROR_CODE_WRONG_KEY -25 #define WALLET_RPC_ERROR_CODE_BAD_HEX -26 #define WALLET_RPC_ERROR_CODE_BAD_TX_METADATA -27 +#define WALLET_RPC_ERROR_CODE_ALREADY_MULTISIG -28 +#define WALLET_RPC_ERROR_CODE_WATCH_ONLY -29 +#define WALLET_RPC_ERROR_CODE_BAD_MULTISIG_INFO -30 +#define WALLET_RPC_ERROR_CODE_NOT_MULTISIG -31 +#define WALLET_RPC_ERROR_CODE_WRONG_LR -32 +#define WALLET_RPC_ERROR_CODE_THRESHOLD_NOT_REACHED -33 +#define WALLET_RPC_ERROR_CODE_BAD_MULTISIG_TX_DATA -34 +#define WALLET_RPC_ERROR_CODE_MULTISIG_SIGNATURE -35 +#define WALLET_RPC_ERROR_CODE_MULTISIG_SUBMISSION -36 |