diff --git a/src/cryptonote_basic/account.cpp b/src/cryptonote_basic/account.cpp index 9cc44f0f0..ddc1fc7fc 100644 --- a/src/cryptonote_basic/account.cpp +++ b/src/cryptonote_basic/account.cpp @@ -64,6 +64,7 @@ DISABLE_VS_WARNINGS(4244 4345) void account_base::forget_spend_key() { m_keys.m_spend_secret_key = crypto::secret_key(); + m_keys.m_multisig_keys.clear(); } //----------------------------------------------------------------- crypto::secret_key account_base::generate(const crypto::secret_key& recovery_key, bool recover, bool two_random) @@ -123,13 +124,20 @@ DISABLE_VS_WARNINGS(4244 4345) create_from_keys(address, fake, viewkey); } //----------------------------------------------------------------- - bool account_base::make_multisig(const crypto::secret_key &view_secret_key, const crypto::public_key &spend_public_key) + bool account_base::make_multisig(const crypto::secret_key &view_secret_key, const crypto::secret_key &spend_secret_key, const crypto::public_key &spend_public_key, const std::vector &multisig_keys) { m_keys.m_account_address.m_spend_public_key = spend_public_key; m_keys.m_view_secret_key = view_secret_key; + m_keys.m_spend_secret_key = spend_secret_key; + m_keys.m_multisig_keys = multisig_keys; return crypto::secret_key_to_public_key(view_secret_key, m_keys.m_account_address.m_view_public_key); } //----------------------------------------------------------------- + void account_base::finalize_multisig(const crypto::public_key &spend_public_key) + { + m_keys.m_account_address.m_spend_public_key = spend_public_key; + } + //----------------------------------------------------------------- const account_keys& account_base::get_keys() const { return m_keys; diff --git a/src/cryptonote_basic/account.h b/src/cryptonote_basic/account.h index ab837df3f..50af36a9d 100644 --- a/src/cryptonote_basic/account.h +++ b/src/cryptonote_basic/account.h @@ -42,11 +42,13 @@ namespace cryptonote account_public_address m_account_address; crypto::secret_key m_spend_secret_key; crypto::secret_key m_view_secret_key; + std::vector m_multisig_keys; BEGIN_KV_SERIALIZE_MAP() KV_SERIALIZE(m_account_address) KV_SERIALIZE_VAL_POD_AS_BLOB_FORCE(m_spend_secret_key) KV_SERIALIZE_VAL_POD_AS_BLOB_FORCE(m_view_secret_key) + KV_SERIALIZE_CONTAINER_POD_AS_BLOB(m_multisig_keys) END_KV_SERIALIZE_MAP() }; @@ -60,7 +62,8 @@ namespace cryptonote crypto::secret_key generate(const crypto::secret_key& recovery_key = crypto::secret_key(), bool recover = false, bool two_random = false); void create_from_keys(const cryptonote::account_public_address& address, const crypto::secret_key& spendkey, const crypto::secret_key& viewkey); void create_from_viewkey(const cryptonote::account_public_address& address, const crypto::secret_key& viewkey); - bool make_multisig(const crypto::secret_key &view_secret_key, const crypto::public_key &spend_public_key); + bool make_multisig(const crypto::secret_key &view_secret_key, const crypto::secret_key &spend_secret_key, const crypto::public_key &spend_public_key, const std::vector &multisig_keys); + void finalize_multisig(const crypto::public_key &spend_public_key); const account_keys& get_keys() const; std::string get_public_address_str(bool testnet) const; std::string get_public_integrated_address_str(const crypto::hash8 &payment_id, bool testnet) const; @@ -72,6 +75,7 @@ namespace cryptonote bool store(const std::string& file_path); void forget_spend_key(); + const std::vector &get_multisig_keys() const { return m_keys.m_multisig_keys; } template inline void serialize(t_archive &a, const unsigned int /*ver*/) diff --git a/src/gen_multisig/gen_multisig.cpp b/src/gen_multisig/gen_multisig.cpp index 358c1e4c0..a9bc7b8fd 100644 --- a/src/gen_multisig/gen_multisig.cpp +++ b/src/gen_multisig/gen_multisig.cpp @@ -99,10 +99,15 @@ static bool generate_multisig(uint32_t threshold, uint32_t total, const std::str std::vector pk(total); for (size_t n = 0; n < total; ++n) { - tools::wallet2::verify_multisig_info(wallets[n]->get_multisig_info(), sk[n], pk[n]); + if (!tools::wallet2::verify_multisig_info(wallets[n]->get_multisig_info(), sk[n], pk[n])) + { + tools::fail_msg_writer() << tr("Failed to verify multisig info"); + return false; + } } // make the wallets multisig + std::vector extra_info(total); std::stringstream ss; for (size_t n = 0; n < total; ++n) { @@ -117,10 +122,33 @@ static bool generate_multisig(uint32_t threshold, uint32_t total, const std::str pkn.push_back(pk[k]); } } - wallets[n]->make_multisig(pwd_container->password(), skn, pkn, threshold); + extra_info[n] = wallets[n]->make_multisig(pwd_container->password(), skn, pkn, threshold); ss << " " << name << std::endl; } + // finalize step if needed + if (!extra_info[0].empty()) + { + std::unordered_set pkeys; + std::vector signers(total); + for (size_t n = 0; n < total; ++n) + { + if (!tools::wallet2::verify_extra_multisig_info(extra_info[n], pkeys, signers[n])) + { + tools::fail_msg_writer() << genms::tr("Error verifying multisig extra info"); + return false; + } + } + for (size_t n = 0; n < total; ++n) + { + if (!wallets[n]->finalize_multisig(pwd_container->password(), pkeys, signers)) + { + tools::fail_msg_writer() << genms::tr("Error finalizing multisig"); + return false; + } + } + } + std::string address = wallets[0]->get_account().get_public_address_str(wallets[0]->testnet()); tools::success_msg_writer() << genms::tr("Generated multisig wallets for address ") << address << std::endl << ss.str(); } diff --git a/src/ringct/rctTypes.h b/src/ringct/rctTypes.h index 1f44c1a9e..5ea2dcc7c 100644 --- a/src/ringct/rctTypes.h +++ b/src/ringct/rctTypes.h @@ -517,6 +517,11 @@ inline std::ostream &operator <<(std::ostream &o, const rct::key &v) { } +namespace std +{ + template<> struct hash { std::size_t operator()(const rct::key &k) const { return reinterpret_cast(k); } }; +} + BLOB_SERIALIZER(rct::key); BLOB_SERIALIZER(rct::key64); BLOB_SERIALIZER(rct::ctkey); diff --git a/src/simplewallet/simplewallet.cpp b/src/simplewallet/simplewallet.cpp index 09dbb1373..ab09ace91 100644 --- a/src/simplewallet/simplewallet.cpp +++ b/src/simplewallet/simplewallet.cpp @@ -825,7 +825,14 @@ bool simple_wallet::make_multisig(const std::vector &args) try { - m_wallet->make_multisig(orig_pwd_container->password(), secret_keys, public_keys, threshold); + std::string multisig_extra_info = m_wallet->make_multisig(orig_pwd_container->password(), secret_keys, public_keys, threshold); + if (!multisig_extra_info.empty()) + { + success_msg_writer() << tr("Another step is needed"); + success_msg_writer() << multisig_extra_info; + success_msg_writer() << tr("Send this multisig info to all other participants, then use finalize_multisig [...] with others' multisig info"); + return true; + } } catch (const std::exception &e) { @@ -840,6 +847,57 @@ bool simple_wallet::make_multisig(const std::vector &args) return true; } +bool simple_wallet::finalize_multisig(const std::vector &args) +{ + if (!m_wallet->multisig()) + { + fail_msg_writer() << tr("This wallet is not multisig"); + return true; + } + + const auto orig_pwd_container = get_and_verify_password(); + if(orig_pwd_container == boost::none) + { + fail_msg_writer() << tr("Your original password was incorrect."); + return true; + } + + if (args.size() < 2) + { + fail_msg_writer() << tr("usage: finalize_multisig [...]"); + return true; + } + + // parse all multisig info + std::unordered_set public_keys; + std::vector signers(args.size(), crypto::null_pkey); + for (size_t i = 0; i < args.size(); ++i) + { + if (!tools::wallet2::verify_extra_multisig_info(args[i], public_keys, signers[i])) + { + fail_msg_writer() << tr("Bad multisig info: ") << args[i]; + return true; + } + } + + // we have all pubkeys now + try + { + if (!m_wallet->finalize_multisig(orig_pwd_container->password(), public_keys, signers)) + { + fail_msg_writer() << tr("Failed to finalize multisig"); + return true; + } + } + catch (const std::exception &e) + { + fail_msg_writer() << tr("Failed to finalize multisig: ") << e.what(); + return true; + } + + return true; +} + bool simple_wallet::export_multisig(const std::vector &args) { if (!m_wallet->multisig()) @@ -869,9 +927,8 @@ bool simple_wallet::export_multisig(const std::vector &args) 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)); - crypto::hash hash; - cn_fast_hash(&m_wallet->get_account().get_keys().m_spend_secret_key, sizeof(crypto::secret_key), (char*)&hash); - header += std::string((const char *)&hash, sizeof(crypto::hash)); + crypto::public_key signer = m_wallet->get_multisig_signer_public_key(); + header += std::string((const char *)&signer, sizeof(crypto::public_key)); std::string ciphertext = m_wallet->encrypt_with_view_secret_key(header + oss.str()); bool r = epee::file_io_utils::save_string_to_file(filename, magic + ciphertext); if (!r) @@ -908,7 +965,7 @@ bool simple_wallet::import_multisig(const std::vector &args) return true; std::vector> info; - std::unordered_set seen; + std::unordered_set seen; for (size_t n = 0; n < args.size(); ++n) { const std::string filename = args[n]; @@ -944,26 +1001,24 @@ bool simple_wallet::import_multisig(const std::vector &args) } 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::hash &hash = *(const crypto::hash*)&data[2*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 = m_wallet->get_account().get_keys().m_account_address; if (public_spend_key != keys.m_spend_public_key || public_view_key != keys.m_view_public_key) { fail_msg_writer() << (boost::format(tr("Multisig info from %s is for a different account")) % filename).str(); return true; } - crypto::hash this_hash; - cn_fast_hash(&m_wallet->get_account().get_keys().m_spend_secret_key, sizeof(crypto::secret_key), (char*)&this_hash); - if (this_hash == hash) + if (m_wallet->get_multisig_signer_public_key() == signer) { message_writer() << (boost::format(tr("Multisig info from %s is from this wallet, ignored")) % filename).str(); continue; } - if (seen.find(hash) != seen.end()) + if (seen.find(signer) != seen.end()) { message_writer() << (boost::format(tr("Multisig info from %s already seen, ignored")) % filename).str(); continue; } - seen.insert(hash); + seen.insert(signer); try { @@ -1721,6 +1776,10 @@ simple_wallet::simple_wallet() m_cmd_binder.set_handler("make_multisig", boost::bind(&simple_wallet::make_multisig, this, _1), tr("make_multisig [...]"), tr("Turn this wallet into a multisig wallet")); + m_cmd_binder.set_handler("finalize_multisig", + boost::bind(&simple_wallet::finalize_multisig, this, _1), + tr("finalize_multisig [...]"), + tr("Turn this wallet into a multisig wallet, extra step for N-1/N wallets")); m_cmd_binder.set_handler("export_multisig_info", boost::bind(&simple_wallet::export_multisig, this, _1), tr("export_multisig "), diff --git a/src/simplewallet/simplewallet.h b/src/simplewallet/simplewallet.h index ebe830f69..73b3456db 100644 --- a/src/simplewallet/simplewallet.h +++ b/src/simplewallet/simplewallet.h @@ -190,6 +190,7 @@ namespace cryptonote bool print_fee_info(const std::vector &args); bool prepare_multisig(const std::vector& args); bool make_multisig(const std::vector& args); + bool finalize_multisig(const std::vector &args); bool export_multisig(const std::vector& args); bool import_multisig(const std::vector& args); bool accept_loaded_tx(const tools::wallet2::multisig_tx_set &txs); diff --git a/src/wallet/wallet2.cpp b/src/wallet/wallet2.cpp index ee5bd0441..807248860 100644 --- a/src/wallet/wallet2.cpp +++ b/src/wallet/wallet2.cpp @@ -1123,8 +1123,6 @@ void wallet2::process_new_transaction(const crypto::hash &txid, const cryptonote error::wallet_internal_error, "NULL m_multisig_rescan_k"); if (m_multisig_rescan_info && m_multisig_rescan_info->front().size() >= m_transfers.size()) update_multisig_rescan_info(*m_multisig_rescan_k, *m_multisig_rescan_info, m_transfers.size() - 1); - else - td.m_multisig_k = rct::skGen(); } LOG_PRINT_L0("Received money: " << print_money(td.amount()) << ", with tx: " << txid); if (0 != m_callback) @@ -1180,8 +1178,6 @@ void wallet2::process_new_transaction(const crypto::hash &txid, const cryptonote error::wallet_internal_error, "NULL m_multisig_rescan_k"); if (m_multisig_rescan_info && m_multisig_rescan_info->front().size() >= m_transfers.size()) update_multisig_rescan_info(*m_multisig_rescan_k, *m_multisig_rescan_info, m_transfers.size() - 1); - else - td.m_multisig_k = rct::skGen(); } THROW_WALLET_EXCEPTION_IF(td.get_public_key() != tx_scan_info[o].in_ephemeral.pub, error::wallet_internal_error, "Inconsistent public keys"); THROW_WALLET_EXCEPTION_IF(td.m_spent, error::wallet_internal_error, "Inconsistent spent status"); @@ -2252,6 +2248,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) @@ -2282,8 +2279,13 @@ bool wallet2::store_keys(const std::string& keys_file_name, const epee::wipeable value2.SetUint(m_multisig_threshold); json.AddMember("multisig_threshold", value2, json.GetAllocator()); - value2.SetUint(m_multisig_total); - json.AddMember("multisig_total", 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()); @@ -2398,7 +2400,7 @@ bool wallet2::load_keys(const std::string& keys_file_name, const epee::wipeable_ m_watch_only = false; m_multisig = false; m_multisig_threshold = 0; - m_multisig_total = 0; + m_multisig_signers.clear(); m_always_confirm_transfers = false; m_print_ring_members = false; m_default_mixin = 0; @@ -2439,8 +2441,27 @@ bool wallet2::load_keys(const std::string& keys_file_name, const epee::wipeable_ 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; - GET_FIELD_FROM_JSON_RETURN_ON_ERROR(json, multisig_total, unsigned int, Uint, m_multisig, 0); - m_multisig_total = field_multisig_total; + 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); @@ -2607,7 +2628,7 @@ crypto::secret_key wallet2::generate(const std::string& wallet_, const epee::wip m_watch_only = false; m_multisig = false; m_multisig_threshold = 0; - m_multisig_total = 0; + m_multisig_signers.clear(); // -1 month for fluctuations in block time and machine date/time setup. // avg seconds per block @@ -2698,7 +2719,7 @@ void wallet2::generate(const std::string& wallet_, const epee::wipeable_string& m_watch_only = true; m_multisig = false; m_multisig_threshold = 0; - m_multisig_total = 0; + m_multisig_signers.clear(); if (!wallet_.empty()) { @@ -2744,7 +2765,7 @@ void wallet2::generate(const std::string& wallet_, const epee::wipeable_string& m_watch_only = false; m_multisig = false; m_multisig_threshold = 0; - m_multisig_total = 0; + m_multisig_signers.clear(); if (!wallet_.empty()) { @@ -2763,7 +2784,7 @@ void wallet2::generate(const std::string& wallet_, const epee::wipeable_string& store(); } -void wallet2::make_multisig(const epee::wipeable_string &password, +std::string wallet2::make_multisig(const epee::wipeable_string &password, const std::vector &view_keys, const std::vector &spend_keys, uint32_t threshold) @@ -2771,45 +2792,92 @@ void wallet2::make_multisig(const epee::wipeable_string &password, CHECK_AND_ASSERT_THROW_MES(!view_keys.empty(), "empty view keys"); CHECK_AND_ASSERT_THROW_MES(view_keys.size() == spend_keys.size(), "Mismatched view/spend key sizes"); CHECK_AND_ASSERT_THROW_MES(threshold > 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"); + 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..."); - rct::key spend_pkey = rct::pk2rct(get_account().get_keys().m_account_address.m_spend_public_key); + std::vector multisig_keys; + rct::key spend_pkey, spend_skey; if (threshold == spend_keys.size() + 1) { // the multisig spend public key is the sum of all spend public keys + spend_pkey = rct::pk2rct(get_account().get_keys().m_account_address.m_spend_public_key); for (const auto &k: spend_keys) rct::addKeys(spend_pkey, spend_pkey, rct::pk2rct(k)); + multisig_keys.push_back(get_account().get_keys().m_spend_secret_key); + spend_skey = rct::sk2rct(get_account().get_keys().m_spend_secret_key); } - else + else if (threshold == spend_keys.size()) { - // the multisig spend public key is the sum of keys derived from all spend public keys - const rct::key spend_skey = rct::sk2rct(get_account().get_keys().m_spend_secret_key); // WRONG + spend_pkey = rct::identity(); + spend_skey = rct::zero(); + + // create all our composite private keys for (const auto &k: spend_keys) { - rct::addKeys(spend_pkey, spend_pkey, rct::scalarmultBase(rct::hash_to_scalar(rct::scalarmultKey(rct::pk2rct(k), spend_skey)))); + rct::keyV data; + data.push_back(rct::scalarmultKey(rct::pk2rct(k), rct::sk2rct(get_account().get_keys().m_spend_secret_key))); + static const rct::key salt = { {'M', 'u', 'l', 't' , 'i', 's', 'i', 'g' , 0x00, 0x00, 0x00,0x00 , 0x00, 0x00, 0x00,0x00 , 0x00, 0x00, 0x00,0x00 , 0x00, 0x00, 0x00,0x00 , 0x00, 0x00, 0x00,0x00 , 0x00, 0x00, 0x00,0x00 } }; + data.push_back(salt); + rct::key msk = rct::hash_to_scalar(data); + multisig_keys.push_back(rct::rct2sk(msk)); + sc_add(spend_skey.bytes, spend_skey.bytes, msk.bytes); } + + // 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; + const crypto::public_key &pkey = get_account().get_keys().m_account_address.m_spend_public_key; + data += std::string((const char *)&pkey, sizeof(crypto::public_key)); + const crypto::public_key signer = get_multisig_signer_public_key(rct::rct2sk(spend_skey)); + 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, pkey, get_account().get_keys().m_spend_secret_key, 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::hash hash; crypto::cn_fast_hash(&get_account().get_keys().m_view_secret_key, sizeof(crypto::secret_key), hash); rct::key view_skey = rct::hash2rct(hash); for (const auto &k: view_keys) sc_add(view_skey.bytes, view_skey.bytes, rct::sk2rct(k).bytes); MINFO("Creating multisig address..."); - CHECK_AND_ASSERT_THROW_MES(m_account.make_multisig(rct::rct2sk(view_skey), rct::rct2pk(spend_pkey)), + CHECK_AND_ASSERT_THROW_MES(m_account.make_multisig(rct::rct2sk(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; - m_multisig_total = spend_keys.size() + 1; + 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(spend_keys.size() + 1, crypto::null_pkey); + } if (!m_wallet_file.empty()) { @@ -2827,6 +2895,65 @@ void wallet2::make_multisig(const epee::wipeable_string &password, if (!m_wallet_file.empty()) store(); + + return extra_multisig_info; +} + +bool wallet2::finalize_multisig(const epee::wipeable_string &password, std::unordered_set pkeys, std::vector signers) +{ + CHECK_AND_ASSERT_THROW_MES(!pkeys.empty(), "empty pkeys"); + + // add ours if not included + const crypto::public_key local_signer = get_multisig_signer_public_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"); + + rct::key spend_public_key = rct::identity(); + for (const auto &pk: pkeys) + { + rct::addKeys(spend_public_key, spend_public_key, rct::pk2rct(pk)); + } + 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)); }); + m_account_public_address.m_spend_public_key = rct::rct2pk(spend_public_key); + m_account.finalize_multisig(m_account_public_address.m_spend_public_key); + + 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::wallet_generate_key_image_helper_export(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, size_t multisig_key_index) const +{ + THROW_WALLET_EXCEPTION_IF(multisig_key_index >= ack.m_multisig_keys.size(), error::wallet_internal_error, "Bad multisig_key_index"); + if (!generate_key_image_helper_old(ack, tx_public_key, real_output_index, in_ephemeral, ki)) + return false; + // we got the ephemeral keypair, but the key image isn't right as it's done as per our private spend key, which is multisig + crypto::generate_key_image(in_ephemeral.pub, ack.m_multisig_keys[multisig_key_index], ki); + return true; } std::string wallet2::get_multisig_info() const @@ -2887,6 +3014,57 @@ bool wallet2::verify_multisig_info(const std::string &data, crypto::secret_key & return true; } +bool wallet2::verify_extra_multisig_info(const std::string &data, std::unordered_set &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::public_key) + sizeof(crypto::signature)) + { + MERROR("Multisig info is corrupt"); + return false; + } + if ((decoded.size() - (sizeof(crypto::public_key) + 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::public_key) + sizeof(crypto::signature))) / sizeof(crypto::public_key); + size_t offset = 0; + const crypto::public_key &pkey = *(const crypto::public_key*)(decoded.data() + offset); + offset += sizeof(pkey); + 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, pkey, 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(uint32_t *threshold, uint32_t *total) const { if (!m_multisig) @@ -2894,7 +3072,7 @@ bool wallet2::multisig(uint32_t *threshold, uint32_t *total) const if (threshold) *threshold = m_multisig_threshold; if (total) - *total = m_multisig_total; + *total = m_multisig_signers.size(); return true; } @@ -3899,7 +4077,7 @@ void wallet2::commit_tx(pending_tx& ptx) // tx generated, get rid of used k values for (size_t idx: ptx.selected_transfers) - m_transfers[idx].m_multisig_k = rct::zero(); + m_transfers[idx].m_multisig_k.clear(); //fee includes dust if dust policy specified it. LOG_PRINT_L1("Transaction successfully sent. <" << txid << ">" << ENDL @@ -4084,7 +4262,6 @@ bool wallet2::sign_tx(unsigned_tx_set &exported_txs, const std::string &signed_f ptx.selected_transfers = sd.selected_transfers; ptx.tx_key = rct::rct2sk(rct::identity()); // don't send it back to the untrusted view wallet ptx.dests = sd.dests; - ptx.msout = msout; ptx.construction_data = sd; txs.push_back(ptx); @@ -4241,7 +4418,7 @@ std::string wallet2::save_multisig_tx(multisig_tx_set txs) // txes generated, get rid of used k values for (size_t n = 0; n < txs.m_ptx.size(); ++n) for (size_t idx: txs.m_ptx[n].construction_data.selected_transfers) - m_transfers[idx].m_multisig_k = rct::zero(); + m_transfers[idx].m_multisig_k.clear(); // zero out some data we don't want to share for (auto &ptx: txs.m_ptx) @@ -4284,9 +4461,15 @@ std::string wallet2::save_multisig_tx(const std::vector& ptx_vector) { multisig_tx_set txs; txs.m_ptx = ptx_vector; - crypto::hash hash; - cn_fast_hash(&get_account().get_keys().m_spend_secret_key, sizeof(crypto::secret_key), (char*)&hash); - txs.m_signers.insert(hash); + + 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); } //---------------------------------------------------------------------------------------------------- @@ -4378,21 +4561,24 @@ bool wallet2::load_multisig_tx_from_file(const std::string &filename, multisig_t return true; } //---------------------------------------------------------------------------------------------------- -bool wallet2::sign_multisig_tx(multisig_tx_set &exported_txs, const std::string &filename, std::vector &txids) +bool wallet2::sign_multisig_tx(multisig_tx_set &exported_txs, std::vector &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(); + 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.msout; + 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); @@ -4406,16 +4592,51 @@ bool wallet2::sign_multisig_tx(multisig_tx_set &exported_txs, const std::string for (const auto &source: sources) indices.push_back(source.real_output); - rct::keyV k; - for (size_t idx: sd.selected_transfers) - k.push_back(get_multisig_k(idx)); + 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)); - THROW_WALLET_EXCEPTION_IF(!rct::signMultisig(ptx.tx.rct_signatures, indices, k, ptx.msout, rct::sk2rct(get_account().get_keys().m_spend_secret_key)), - error::wallet_internal_error, "Failed signing, transaction likely malformed"); + 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()) { @@ -4429,12 +4650,18 @@ bool wallet2::sign_multisig_tx(multisig_tx_set &exported_txs, const std::string // txes generated, get rid of used k values for (size_t n = 0; n < exported_txs.m_ptx.size(); ++n) for (size_t idx: exported_txs.m_ptx[n].construction_data.selected_transfers) - m_transfers[idx].m_multisig_k = rct::zero(); + m_transfers[idx].m_multisig_k.clear(); - crypto::hash hash; - cn_fast_hash(&get_account().get_keys().m_spend_secret_key, sizeof(crypto::secret_key), (char*)&hash); - exported_txs.m_signers.insert(hash); + exported_txs.m_signers.insert(get_multisig_signer_public_key()); + return true; +} +//---------------------------------------------------------------------------------------------------- +bool wallet2::sign_multisig_tx_from_file(multisig_tx_set &exported_txs, const std::string &filename, std::vector &txids) +{ + bool r = sign_multisig_tx(exported_txs, txids); + if (!r) + return false; return save_multisig_tx(exported_txs, filename); } //---------------------------------------------------------------------------------------------------- @@ -4444,9 +4671,8 @@ bool wallet2::sign_multisig_tx_from_file(const std::string &filename, std::vecto if(!load_multisig_tx_from_file(filename, exported_txs)) return false; - crypto::hash hash; - cn_fast_hash(&get_account().get_keys().m_spend_secret_key, sizeof(crypto::secret_key), (char*)&hash); - THROW_WALLET_EXCEPTION_IF(exported_txs.m_signers.find(hash) != exported_txs.m_signers.end(), + const crypto::public_key signer = get_multisig_signer_public_key(); + THROW_WALLET_EXCEPTION_IF(exported_txs.m_signers.find(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"); @@ -4458,7 +4684,7 @@ bool wallet2::sign_multisig_tx_from_file(const std::string &filename, std::vecto LOG_PRINT_L1("Transactions rejected by callback"); return false; } - return sign_multisig_tx(exported_txs, filename, txids); + return sign_multisig_tx_from_file(exported_txs, filename, txids); } //---------------------------------------------------------------------------------------------------- uint64_t wallet2::get_fee_multiplier(uint32_t priority, int fee_algorithm) @@ -5012,6 +5238,8 @@ void wallet2::transfer_selected(const std::vector 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) { @@ -5207,6 +5461,7 @@ void wallet2::transfer_selected_rct(std::vector sources; + std::unordered_set used_L; for(size_t idx: selected_transfers) { sources.resize(sources.size()+1); @@ -5249,7 +5504,10 @@ void wallet2::transfer_selected_rct(std::vector 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 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, &msout); + LOG_PRINT_L2("constructed tx, r="< bool @@ -5329,8 +5626,8 @@ void wallet2::transfer_selected_rct(std::vector &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_LRki(size_t n, const rct::key &k) const +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; rct::scalarmultBase(kLRki.L, kLRki.k); @@ -8030,27 +8363,77 @@ rct::multisig_kLRki wallet2::get_multisig_LRki(size_t n, const rct::key &k) cons return kLRki; } //---------------------------------------------------------------------------------------------------- -rct::multisig_kLRki wallet2::get_multisig_composite_LRki(size_t n, const rct::key &k) const +rct::multisig_kLRki wallet2::get_multisig_composite_kLRki(size_t n, const crypto::public_key &ignore, std::unordered_set &used_L, std::unordered_set &new_used_L) const { - rct::multisig_kLRki kLRki = get_multisig_LRki(n, k); + 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]; crypto::public_key tx_key = get_tx_pub_key_from_received_outs(td); cryptonote::keypair in_ephemeral; - bool r = wallet_generate_key_image_helper_old(get_account().get_keys(), tx_key, td.m_internal_output_index, in_ephemeral, (crypto::key_image&)kLRki.ki); + crypto::key_image ki; + bool r = wallet_generate_key_image_helper_old(get_account().get_keys(), tx_key, td.m_internal_output_index, in_ephemeral, ki); CHECK_AND_ASSERT_THROW_MES(r, "Failed to generate key image"); + std::unordered_set used; + + // insert the ones we start from + for (size_t m = 0; m < get_account().get_multisig_keys().size(); ++m) + { + crypto::key_image pki; + wallet_generate_key_image_helper_export(get_account().get_keys(), tx_key, td.m_internal_output_index, in_ephemeral, pki, m); + used.insert(pki); + } + for (const auto &info: td.m_multisig_info) { - rct::addKeys(kLRki.ki, kLRki.ki, rct::ki2rct(info.m_partial_key_image)); - rct::addKeys(kLRki.L, kLRki.L, info.m_L); - rct::addKeys(kLRki.R, kLRki.R, info.m_R); + for (const auto &pki: info.m_partial_key_images) + { + // don't add duplicates again + if (used.find(pki) != used.end()) + continue; + used.insert(pki); + + rct::addKeys((rct::key&)ki, rct::ki2rct(ki), rct::ki2rct(pki)); + } } - return kLRki; + return ki; } //---------------------------------------------------------------------------------------------------- std::vector wallet2::export_multisig() { std::vector 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) { @@ -8058,21 +8441,33 @@ std::vector wallet2::export_multisig() crypto::public_key tx_key = get_tx_pub_key_from_received_outs(td); cryptonote::keypair in_ephemeral; crypto::key_image ki; - td.m_multisig_k = rct::skGen(); - const rct::multisig_kLRki kLRki = get_multisig_LRki(n, td.m_multisig_k); - const std::vector additional_tx_pub_keys = get_additional_tx_pub_keys_from_extra(td.m_tx); - // we want to export the partial key image, not the full one, so we can't use td.m_key_image - bool r = wallet_generate_key_image_helper_old(get_account().get_keys(), tx_key, td.m_internal_output_index, in_ephemeral, ki, true); - CHECK_AND_ASSERT_THROW_MES(r, "Failed to generate key image"); - // we got the ephemeral keypair, but the key image isn't right as it's done as per our private spend key, which is multisig - crypto::generate_key_image(in_ephemeral.pub, get_account().get_keys().m_spend_secret_key, ki); - info[n] = multisig_info({ki, kLRki.L, kLRki.R}); + 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 = wallet_generate_key_image_helper_export(get_account().get_keys(), tx_key, td.m_internal_output_index, in_ephemeral, ki, m); + 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; } return info; } //---------------------------------------------------------------------------------------------------- -void wallet2::update_multisig_rescan_info(const std::vector &multisig_k, const std::vector> &info, size_t n) +void wallet2::update_multisig_rescan_info(const std::vector> &multisig_k, const std::vector> &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"); @@ -8086,7 +8481,7 @@ void wallet2::update_multisig_rescan_info(const std::vector &multisig_ td.m_multisig_info.push_back(pi[n]); } m_key_images.erase(td.m_key_image); - td.m_key_image = rct::rct2ki(get_multisig_composite_LRki(n, rct::skGen()).ki); + td.m_key_image = 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]; @@ -8096,9 +8491,9 @@ void wallet2::update_multisig_rescan_info(const std::vector &multisig_ size_t wallet2::import_multisig(std::vector> info) { CHECK_AND_ASSERT_THROW_MES(m_multisig, "Wallet is not multisig"); - CHECK_AND_ASSERT_THROW_MES(info.size() + 1 == m_multisig_total, "Wrong number of multisig sources"); + 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 k; + std::vector> k; k.reserve(m_transfers.size()); for (const auto &td: m_transfers) k.push_back(td.m_multisig_k); @@ -8109,10 +8504,28 @@ size_t wallet2::import_multisig(std::vector &i0, const std::vector &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) { diff --git a/src/wallet/wallet2.h b/src/wallet/wallet2.h index 8567edc9a..5f973fef5 100644 --- a/src/wallet/wallet2.h +++ b/src/wallet/wallet2.h @@ -172,14 +172,25 @@ namespace tools struct multisig_info { - crypto::key_image m_partial_key_image; - rct::key m_L; - rct::key m_R; + 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 m_LR; + std::vector m_partial_key_images; // one per key the participant has BEGIN_SERIALIZE_OBJECT() - FIELD(m_partial_key_image) - FIELD(m_L) - FIELD(m_R) + FIELD(m_signer) + FIELD(m_LR) + FIELD(m_partial_key_images) END_SERIALIZE() }; @@ -213,8 +224,8 @@ namespace tools size_t m_pk_index; cryptonote::subaddress_index m_subaddr_index; bool m_key_image_partial; - rct::key m_multisig_k; - std::vector m_multisig_info; + std::vector m_multisig_k; + std::vector m_multisig_info; // one per other participant bool is_rct() const { return m_rct; } uint64_t amount() const { return m_amount; } @@ -327,6 +338,15 @@ namespace tools typedef std::vector transfer_container; typedef std::unordered_multimap payment_container; + struct multisig_sig + { + rct::rctSig sigs; + crypto::public_key ignore; + std::unordered_set used_L; + std::unordered_set signing_keys; + rct::multisig_out msout; + }; + // The convention for destinations is: // dests does not include change // splitted_dsts (in construction_data) does @@ -341,7 +361,7 @@ namespace tools crypto::secret_key tx_key; std::vector additional_tx_keys; std::vector dests; - rct::multisig_out msout; + std::vector multisig_sigs; tx_construction_data construction_data; @@ -357,6 +377,7 @@ namespace tools FIELD(additional_tx_keys) FIELD(dests) FIELD(construction_data) + FIELD(multisig_sigs) END_SERIALIZE() }; @@ -377,7 +398,7 @@ namespace tools struct multisig_tx_set { std::vector m_ptx; - std::unordered_set m_signers; + std::unordered_set m_signers; }; struct keys_file_data @@ -446,11 +467,17 @@ namespace tools 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 */ - void make_multisig(const epee::wipeable_string &password, + std::string make_multisig(const epee::wipeable_string &password, const std::vector &view_keys, const std::vector &spend_keys, uint32_t threshold); + /*! + * \brief Finalizes creation of a multisig wallet + */ + bool finalize_multisig(const epee::wipeable_string &password, std::unordered_set pkeys, std::vector signers); /*! * Get a packaged multisig information string */ @@ -459,6 +486,10 @@ namespace tools * 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 &pkeys, crypto::public_key &signer); /*! * Export multisig info * This will generate and remember new k values @@ -610,7 +641,8 @@ namespace tools std::vector create_transactions_from(const cryptonote::account_public_address &address, bool is_subaddress, std::vector unused_transfers_indices, std::vector unused_dust_indices, const size_t fake_outs_count, const uint64_t unlock_time, uint32_t priority, const std::vector& extra, bool trusted_daemon); bool load_multisig_tx_from_file(const std::string &filename, multisig_tx_set &exported_txs, std::function accept_func = NULL); bool sign_multisig_tx_from_file(const std::string &filename, std::vector &txids, std::function accept_func); - bool sign_multisig_tx(multisig_tx_set &exported_txs, const std::string &filename, std::vector &txids); + bool sign_multisig_tx(multisig_tx_set &exported_txs, std::vector &txids); + bool sign_multisig_tx_from_file(multisig_tx_set &exported_txs, const std::string &filename, std::vector &txids); std::vector 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; @@ -890,6 +922,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. @@ -936,15 +973,17 @@ namespace tools void set_unspent(size_t idx); void get_outs(std::vector> &outs, const std::vector &selected_transfers, size_t fake_outputs_count); bool tx_add_fake_output(std::vector> &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_export(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, size_t multisig_key_index) const; 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 &unused_transfers_indices, const std::vector &unused_dust_indices) const; std::vector get_only_rct(const std::vector &unused_dust_indices, const std::vector &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 &tx_money_got_in_outs, std::vector &outs); void trim_hashchain(); - rct::multisig_kLRki get_multisig_composite_LRki(size_t n, const rct::key &k) const; - rct::multisig_kLRki get_multisig_LRki(size_t n, const rct::key &k) const; - rct::key get_multisig_k(size_t idx) const; - void update_multisig_rescan_info(const std::vector &multisig_k, const std::vector> &info, size_t n); + 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 &used_L, std::unordered_set &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 &used_L) const; + void update_multisig_rescan_info(const std::vector> &multisig_k, const std::vector> &info, size_t n); cryptonote::account_base m_account; boost::optional m_daemon_login; @@ -974,7 +1013,7 @@ namespace tools std::vector 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> *m_multisig_rescan_info; - const std::vector *m_multisig_rescan_k; + const std::vector> *m_multisig_rescan_k; std::atomic m_run; @@ -988,7 +1027,7 @@ namespace tools 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; - uint32_t m_multisig_total; + std::vector 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 */ @@ -1026,6 +1065,7 @@ namespace tools BOOST_CLASS_VERSION(tools::wallet2, 22) 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) @@ -1036,6 +1076,7 @@ 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, 3) +BOOST_CLASS_VERSION(tools::wallet2::multisig_sig, 0) namespace boost { @@ -1076,8 +1117,8 @@ namespace boost if (ver < 9) { x.m_key_image_partial = false; + x.m_multisig_k.clear(); x.m_multisig_info.clear(); - x.m_multisig_k = rct::zero(); } } @@ -1163,13 +1204,20 @@ namespace boost } template - inline void serialize(Archive &a, tools::wallet2::multisig_info &x, const boost::serialization::version_type ver) + inline void serialize(Archive &a, tools::wallet2::multisig_info::LR &x, const boost::serialization::version_type ver) { - a & x.m_partial_key_image; a & x.m_L; a & x.m_R; } + template + 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 inline void serialize(Archive &a, tools::wallet2::multisig_tx_set &x, const boost::serialization::version_type ver) { @@ -1352,6 +1400,16 @@ namespace boost a & x.selected_transfers; } + template + 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 inline void serialize(Archive &a, tools::wallet2::pending_tx &x, const boost::serialization::version_type ver) { @@ -1382,7 +1440,7 @@ namespace boost a & x.selected_transfers; if (ver < 3) return; - a & x.msout; + a & x.multisig_sigs; } } } @@ -1458,6 +1516,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; @@ -1560,10 +1620,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; - if (m_multisig) - src.multisig_kLRki = get_multisig_composite_LRki(idx, get_multisig_k(idx)); - else - src.multisig_kLRki = rct::multisig_kLRki({rct::zero(), rct::zero(), rct::zero(), rct::zero()}); + src.multisig_kLRki = rct::multisig_kLRki({rct::zero(), rct::zero(), rct::zero(), rct::zero()}); detail::print_source_entry(src); ++i; } @@ -1619,7 +1676,6 @@ namespace tools ptx.tx_key = tx_key; ptx.additional_tx_keys = additional_tx_keys; ptx.dests = dsts; - ptx.msout = msout; ptx.construction_data.sources = sources; ptx.construction_data.change_dts = change_dts; ptx.construction_data.splitted_dsts = splitted_dsts; diff --git a/src/wallet/wallet_rpc_server.cpp b/src/wallet/wallet_rpc_server.cpp index e2bb0a5c6..472302a94 100644 --- a/src/wallet/wallet_rpc_server.cpp +++ b/src/wallet/wallet_rpc_server.cpp @@ -2446,7 +2446,7 @@ namespace tools try { - m_wallet->make_multisig(req.password, secret_keys, public_keys, req.threshold); + res.multisig_info = m_wallet->make_multisig(req.password, secret_keys, public_keys, req.threshold); res.address = m_wallet->get_account().get_public_address_str(m_wallet->testnet()); } catch (const std::exception &e) @@ -2490,9 +2490,16 @@ namespace tools res.info.resize(info.size()); for (size_t n = 0; n < info.size(); ++n) { - res.info[n].partial_key_image = epee::string_tools::pod_to_hex(info[n].m_partial_key_image); - res.info[n].L = epee::string_tools::pod_to_hex(info[n].m_L); - res.info[n].R = epee::string_tools::pod_to_hex(info[n].m_R); + res.info[n].signer = epee::string_tools::pod_to_hex(info[n].m_signer); + res.info[n].LR.resize(info[n].m_LR.size()); + for (size_t l = 0; l < info[n].m_LR.size(); ++l) + { + res.info[n].LR[l].L = epee::string_tools::pod_to_hex(info[n].m_LR[l].m_L); + res.info[n].LR[l].R = epee::string_tools::pod_to_hex(info[n].m_LR[l].m_R); + } + res.info[n].partial_key_images.resize(info[n].m_partial_key_images.size()); + for (size_t l = 0; l < info[n].m_partial_key_images.size(); ++l) + res.info[n].partial_key_images[l] = epee::string_tools::pod_to_hex(info[n].m_partial_key_images[l]); } return true; @@ -2529,17 +2536,34 @@ namespace tools info[n].resize(req.info[n].info.size()); for (size_t i = 0; i < info[n].size(); ++i) { - if (!epee::string_tools::hex_to_pod(req.info[n].info[i].partial_key_image, info[n][i].m_partial_key_image)) + const auto &src = req.info[n].info[i]; + auto &dst = info[n][i]; + + if (!epee::string_tools::hex_to_pod(src.signer, dst.m_signer)) { - er.code = WALLET_RPC_ERROR_CODE_WRONG_KEY_IMAGE; - er.message = "Failed to parse partial key image from multisig info"; + er.code = WALLET_RPC_ERROR_CODE_WRONG_ADDRESS; + er.message = "Failed to parse signer from multisig info"; return false; } - if (!epee::string_tools::hex_to_pod(req.info[n].info[i].L, info[n][i].m_L) || !epee::string_tools::hex_to_pod(req.info[n].info[i].R, info[n][i].m_R)) + dst.m_LR.resize(src.LR.size()); + for (size_t l = 0; l < src.LR.size(); ++l) { - er.code = WALLET_RPC_ERROR_CODE_WRONG_LR; - er.message = "Failed to parse L/R info from hex"; - return false; + if (!epee::string_tools::hex_to_pod(src.LR[l].L, dst.m_LR[l].m_L) || !epee::string_tools::hex_to_pod(src.LR[l].R, dst.m_LR[l].m_R)) + { + er.code = WALLET_RPC_ERROR_CODE_WRONG_LR; + er.message = "Failed to parse L/R from multisig info"; + return false; + } + } + dst.m_partial_key_images.resize(src.partial_key_images.size()); + for (size_t l = 0; l < src.partial_key_images.size(); ++l) + { + if (!epee::string_tools::hex_to_pod(src.partial_key_images[l], dst.m_partial_key_images[l])) + { + er.code = WALLET_RPC_ERROR_CODE_WRONG_KEY_IMAGE; + er.message = "Failed to parse partial key image from multisig info"; + return false; + } } } } @@ -2573,6 +2597,64 @@ namespace tools 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; + } + uint32_t threshold, total; + if (!m_wallet->multisig(&threshold, &total)) + { + er.code = WALLET_RPC_ERROR_CODE_NOT_MULTISIG; + er.message = "This wallet is not multisig"; + 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; + } + + // parse all multisig info + std::unordered_set public_keys; + std::vector signers(req.multisig_info.size(), crypto::null_pkey); + for (size_t i = 0; i < req.multisig_info.size(); ++i) + { + if (!m_wallet->verify_extra_multisig_info(req.multisig_info[i], public_keys, signers[i])) + { + er.code = WALLET_RPC_ERROR_CODE_BAD_MULTISIG_INFO; + er.message = std::string("Bad multisig_info info: ") + req.multisig_info[i]; + return false; + } + } + + try + { + if (!m_wallet->finalize_multisig(req.password, public_keys, signers)) + { + 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; + } + //------------------------------------------------------------------------------------------------------------------------------ } int main(int argc, char** argv) { diff --git a/src/wallet/wallet_rpc_server.h b/src/wallet/wallet_rpc_server.h index 10b2e9dbd..22eb8964e 100644 --- a/src/wallet/wallet_rpc_server.h +++ b/src/wallet/wallet_rpc_server.h @@ -181,6 +181,7 @@ namespace tools 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); //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 806704d6e..d17af9980 100644 --- a/src/wallet/wallet_rpc_server_commands_defs.h +++ b/src/wallet/wallet_rpc_server_commands_defs.h @@ -1546,26 +1546,39 @@ namespace wallet_rpc 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 multisig_info_entry + struct LR_entry { - std::string partial_key_image; std::string L; std::string R; BEGIN_KV_SERIALIZE_MAP() - KV_SERIALIZE(partial_key_image) KV_SERIALIZE(L) KV_SERIALIZE(R) END_KV_SERIALIZE_MAP() }; + struct multisig_info_entry + { + std::string signer; + std::vector LR; + std::vector partial_key_images; + + BEGIN_KV_SERIALIZE_MAP() + KV_SERIALIZE(signer) + KV_SERIALIZE(LR) + KV_SERIALIZE(partial_key_images) + END_KV_SERIALIZE_MAP() + }; + struct COMMAND_RPC_EXPORT_MULTISIG { struct request @@ -1613,5 +1626,29 @@ namespace wallet_rpc END_KV_SERIALIZE_MAP() }; }; + + struct COMMAND_RPC_FINALIZE_MULTISIG + { + struct request + { + std::string password; + std::vector 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() + }; + }; + } } diff --git a/tests/unit_tests/multisig.cpp b/tests/unit_tests/multisig.cpp index 4a105c51c..0e8a6b168 100644 --- a/tests/unit_tests/multisig.cpp +++ b/tests/unit_tests/multisig.cpp @@ -125,14 +125,26 @@ static void make_M_3_wallet(tools::wallet2 &wallet0, tools::wallet2 &wallet1, to ASSERT_TRUE(tools::wallet2::verify_multisig_info(mi0, sk2[0], pk2[0])); ASSERT_TRUE(tools::wallet2::verify_multisig_info(mi1, sk2[1], pk2[1])); - // not implemented yet - if (M < 3) - return; - ASSERT_FALSE(wallet0.multisig() || wallet1.multisig() || wallet2.multisig()); - wallet0.make_multisig("", sk0, pk0, M); - wallet1.make_multisig("", sk1, pk1, M); - wallet2.make_multisig("", sk2, pk2, M); + std::string mxi0 = wallet0.make_multisig("", sk0, pk0, M); + std::string mxi1 = wallet1.make_multisig("", sk1, pk1, M); + std::string mxi2 = wallet2.make_multisig("", sk2, pk2, M); + + const size_t nset = !mxi0.empty() + !mxi1.empty() + !mxi2.empty(); + ASSERT_TRUE((M < 3 && nset == 3) || (M == 3 && nset == 0)); + + if (nset > 0) + { + std::unordered_set pkeys; + std::vector signers(3, crypto::null_pkey); + ASSERT_TRUE(tools::wallet2::verify_extra_multisig_info(mxi0, pkeys, signers[0])); + ASSERT_TRUE(tools::wallet2::verify_extra_multisig_info(mxi1, pkeys, signers[1])); + ASSERT_TRUE(tools::wallet2::verify_extra_multisig_info(mxi2, pkeys, signers[2])); + ASSERT_TRUE(pkeys.size() == 3); + ASSERT_TRUE(wallet0.finalize_multisig("", pkeys, signers)); + ASSERT_TRUE(wallet1.finalize_multisig("", pkeys, signers)); + ASSERT_TRUE(wallet2.finalize_multisig("", pkeys, signers)); + } ASSERT_TRUE(wallet0.get_account().get_public_address_str(true) == wallet1.get_account().get_public_address_str(true)); ASSERT_TRUE(wallet0.get_account().get_public_address_str(true) == wallet2.get_account().get_public_address_str(true));