diff --git a/src/wallet/api/pending_transaction.cpp b/src/wallet/api/pending_transaction.cpp index ff4619f0f..8d200220d 100644 --- a/src/wallet/api/pending_transaction.cpp +++ b/src/wallet/api/pending_transaction.cpp @@ -34,6 +34,7 @@ #include "cryptonote_basic/cryptonote_format_utils.h" #include "cryptonote_basic/cryptonote_basic_impl.h" +#include "common/base58.h" #include #include @@ -102,6 +103,11 @@ bool PendingTransactionImpl::commit(const std::string &filename, bool overwrite) } // Commit tx else { + auto multisigState = m_wallet.multisig(); + if (multisigState.isMultisig && m_signers.size() < multisigState.threshold) { + throw runtime_error("Not enough signers to send multisig transaction"); + } + m_wallet.pauseRefresh(); while (!m_pending_tx.empty()) { auto & ptx = m_pending_tx.back(); @@ -188,6 +194,53 @@ std::vector> PendingTransactionImpl::subaddrIndices() const return result; } +std::string PendingTransactionImpl::multisigSignData() { + try { + if (!m_wallet.multisig().isMultisig) { + throw std::runtime_error("wallet is not multisig"); + } + + auto cipher = m_wallet.m_wallet->save_multisig_tx(m_pending_tx); + return epee::string_tools::buff_to_hex_nodelimer(cipher); + } catch (const std::exception& e) { + m_status = Status_Error; + m_errorString = std::string(tr("Couldn't multisig sign data: ")) + e.what(); + } + + return std::string(); +} + +void PendingTransactionImpl::signMultisigTx() { + try { + std::vector ignore; + + tools::wallet2::multisig_tx_set txSet; + txSet.m_ptx = m_pending_tx; + txSet.m_signers = m_signers; + + if (!m_wallet.m_wallet->sign_multisig_tx(txSet, ignore)) { + throw std::runtime_error("couldn't sign multisig transaction"); + } + + std::swap(m_pending_tx, txSet.m_ptx); + std::swap(m_signers, txSet.m_signers); + } catch (const std::exception& e) { + m_status = Status_Error; + m_errorString = std::string(tr("Couldn't sign multisig transaction: ")) + e.what(); + } +} + +std::vector PendingTransactionImpl::signersKeys() const { + std::vector keys; + keys.reserve(m_signers.size()); + + for (const auto& signer: m_signers) { + keys.emplace_back(tools::base58::encode(cryptonote::t_serializable_object_to_blob(signer))); + } + + return keys; +} + } namespace Bitmonero = Monero; diff --git a/src/wallet/api/pending_transaction.h b/src/wallet/api/pending_transaction.h index d0bd66eb5..4f963c134 100644 --- a/src/wallet/api/pending_transaction.h +++ b/src/wallet/api/pending_transaction.h @@ -55,6 +55,10 @@ public: std::vector> subaddrIndices() const; // TODO: continue with interface; + std::string multisigSignData(); + void signMultisigTx(); + std::vector signersKeys() const; + private: friend class WalletImpl; WalletImpl &m_wallet; @@ -62,6 +66,7 @@ private: int m_status; std::string m_errorString; std::vector m_pending_tx; + std::unordered_set m_signers; }; diff --git a/src/wallet/api/wallet.cpp b/src/wallet/api/wallet.cpp index 36c895aca..ade7b461c 100644 --- a/src/wallet/api/wallet.cpp +++ b/src/wallet/api/wallet.cpp @@ -77,6 +77,36 @@ namespace { dir /= ".shared-ringdb"; return dir.string(); } + + void checkMultisigWalletReady(const tools::wallet2* wallet) { + if (!wallet) { + throw runtime_error("Wallet is not initialized yet"); + } + + bool ready; + if (!wallet->multisig(&ready)) { + throw runtime_error("Wallet is not multisig"); + } + + if (!ready) { + throw runtime_error("Multisig wallet is not finalized yet"); + } + } + + void checkMultisigWalletNotReady(const tools::wallet2* wallet) { + if (!wallet) { + throw runtime_error("Wallet is not initialized yet"); + } + + bool ready; + if (!wallet->multisig(&ready)) { + throw runtime_error("Wallet is not multisig"); + } + + if (ready) { + throw runtime_error("Multisig wallet is already finalized"); + } + } } struct Wallet2CallbackImpl : public tools::i_wallet2_callback @@ -1058,6 +1088,132 @@ void WalletImpl::setSubaddressLabel(uint32_t accountIndex, uint32_t addressIndex } } +MultisigState WalletImpl::multisig() const { + MultisigState state; + state.isMultisig = m_wallet->multisig(&state.isReady, &state.threshold, &state.total); + + return state; +} + +string WalletImpl::getMultisigInfo() const { + try { + clearStatus(); + return m_wallet->get_multisig_info(); + } catch (const exception& e) { + LOG_ERROR("Error on generating multisig info: ") << e.what(); + setStatusError(string(tr("Failed to get multisig info: ")) + e.what()); + } + + return string(); +} + +string WalletImpl::makeMultisig(const vector& info, uint32_t threshold) { + try { + clearStatus(); + + if (m_wallet->multisig()) { + throw runtime_error("Wallet is already multisig"); + } + + return m_wallet->make_multisig(epee::wipeable_string(m_password), info, threshold); + } catch (const exception& e) { + LOG_ERROR("Error on making multisig wallet: ") << e.what(); + setStatusError(string(tr("Failed to make multisig: ")) + e.what()); + } + + return string(); +} + +bool WalletImpl::finalizeMultisig(const vector& extraMultisigInfo) { + try { + clearStatus(); + checkMultisigWalletNotReady(m_wallet); + + if (m_wallet->finalize_multisig(epee::wipeable_string(m_password), extraMultisigInfo)) { + return true; + } + + setStatusError(tr("Failed to finalize multisig wallet creation")); + } catch (const exception& e) { + LOG_ERROR("Error on finalizing multisig wallet creation: ") << e.what(); + setStatusError(string(tr("Failed to finalize multisig wallet creation: ")) + e.what()); + } + + return false; +} + +bool WalletImpl::exportMultisigImages(string& images) { + try { + clearStatus(); + checkMultisigWalletReady(m_wallet); + + auto blob = m_wallet->export_multisig(); + images = epee::string_tools::buff_to_hex_nodelimer(blob); + return true; + } catch (const exception& e) { + LOG_ERROR("Error on exporting multisig images: ") << e.what(); + setStatusError(string(tr("Failed to export multisig images: ")) + e.what()); + } + + return false; +} + +size_t WalletImpl::importMultisigImages(const vector& images) { + try { + clearStatus(); + checkMultisigWalletReady(m_wallet); + + std::vector blobs; + blobs.reserve(images.size()); + + for (const auto& image: images) { + std::string blob; + if (!epee::string_tools::parse_hexstr_to_binbuff(image, blob)) { + LOG_ERROR("Failed to parse imported multisig images"); + setStatusError(tr("Failed to parse imported multisig images")); + return 0; + } + + blobs.emplace_back(std::move(blob)); + } + + return m_wallet->import_multisig(blobs); + } catch (const exception& e) { + LOG_ERROR("Error on importing multisig images: ") << e.what(); + setStatusError(string(tr("Failed to import multisig images: ")) + e.what()); + } + + return 0; +} + +PendingTransaction* WalletImpl::restoreMultisigTransaction(const string& signData) { + try { + clearStatus(); + checkMultisigWalletReady(m_wallet); + + string binary; + if (!epee::string_tools::parse_hexstr_to_binbuff(signData, binary)) { + throw runtime_error("Failed to deserialize multisig transaction"); + } + + tools::wallet2::multisig_tx_set txSet; + if (!m_wallet->load_multisig_tx(binary, txSet, {})) { + throw runtime_error("couldn't parse multisig transaction data"); + } + + auto ptx = new PendingTransactionImpl(*this); + ptx->m_pending_tx = txSet.m_ptx; + ptx->m_signers = txSet.m_signers; + + return ptx; + } catch (exception& e) { + LOG_ERROR("Error on restoring multisig transaction: ") << e.what(); + setStatusError(string(tr("Failed to restore multisig transaction: ")) + e.what()); + } + + return nullptr; +} + // TODO: // 1 - properly handle payment id (add another menthod with explicit 'payment_id' param) // 2 - check / design how "Transaction" can be single interface @@ -1095,6 +1251,7 @@ PendingTransaction *WalletImpl::createTransaction(const string &dst_addr, const break; } + std::vector extra; // if dst_addr is not an integrated address, parse payment_id if (!info.has_payment_id && !payment_id.empty()) { @@ -1155,6 +1312,9 @@ PendingTransaction *WalletImpl::createTransaction(const string &dst_addr, const extra, subaddr_account, subaddr_indices, m_trustedDaemon); } + if (multisig().isMultisig) { + transaction->m_signers = m_wallet->make_multisig_tx_set(transaction->m_pending_tx).m_signers; + } } catch (const tools::error::daemon_busy&) { // TODO: make it translatable with "tr"? setStatusError(tr("daemon is busy. Please try again later.")); diff --git a/src/wallet/api/wallet.h b/src/wallet/api/wallet.h index 7a0526e0a..e0e627c36 100644 --- a/src/wallet/api/wallet.h +++ b/src/wallet/api/wallet.h @@ -127,6 +127,14 @@ public: std::string getSubaddressLabel(uint32_t accountIndex, uint32_t addressIndex) const; void setSubaddressLabel(uint32_t accountIndex, uint32_t addressIndex, const std::string &label); + MultisigState multisig() const override; + std::string getMultisigInfo() const override; + std::string makeMultisig(const std::vector& info, uint32_t threshold) override; + bool finalizeMultisig(const std::vector& extraMultisigInfo) override; + bool exportMultisigImages(std::string& images) override; + size_t importMultisigImages(const std::vector& images) override; + PendingTransaction* restoreMultisigTransaction(const std::string& signData) override; + PendingTransaction * createTransaction(const std::string &dst_addr, const std::string &payment_id, optional amount, uint32_t mixin_count, PendingTransaction::Priority priority = PendingTransaction::Priority_Low, diff --git a/src/wallet/api/wallet2_api.h b/src/wallet/api/wallet2_api.h index 6ffef4400..f762f37c5 100644 --- a/src/wallet/api/wallet2_api.h +++ b/src/wallet/api/wallet2_api.h @@ -100,6 +100,30 @@ struct PendingTransaction virtual uint64_t txCount() const = 0; virtual std::vector subaddrAccount() const = 0; virtual std::vector> subaddrIndices() const = 0; + + /** + * @brief multisigSignData + * @return encoded multisig transaction with signers' keys. + * Transfer this data to another wallet participant to sign it. + * Assumed use case is: + * 1. Initiator: + * auto data = pendingTransaction->multisigSignData(); + * 2. Signer1: + * pendingTransaction = wallet->restoreMultisigTransaction(data); + * pendingTransaction->signMultisigTx(); + * auto signed = pendingTransaction->multisigSignData(); + * 3. Signer2: + * pendingTransaction = wallet->restoreMultisigTransaction(signed); + * pendingTransaction->signMultisigTx(); + * pendingTransaction->commit(); + */ + virtual std::string multisigSignData() = 0; + virtual void signMultisigTx() = 0; + /** + * @brief signersKeys + * @return vector of base58-encoded signers' public keys + */ + virtual std::vector signersKeys() const = 0; }; /** @@ -291,6 +315,15 @@ struct SubaddressAccount virtual void refresh() = 0; }; +struct MultisigState { + MultisigState() : isMultisig(false), isReady(false), threshold(0), total(0) {} + + bool isMultisig; + bool isReady; + uint32_t threshold; + uint32_t total; +}; + struct WalletListener { virtual ~WalletListener() = 0; @@ -630,6 +663,48 @@ struct Wallet */ virtual void setSubaddressLabel(uint32_t accountIndex, uint32_t addressIndex, const std::string &label) = 0; + /** + * @brief multisig - returns current state of multisig wallet creation process + * @return MultisigState struct + */ + virtual MultisigState multisig() const = 0; + /** + * @brief getMultisigInfo + * @return serialized and signed multisig info string + */ + virtual std::string getMultisigInfo() const = 0; + /** + * @brief makeMultisig - switches wallet in multisig state. The one and only creation phase for N / N wallets + * @param info - vector of multisig infos from other participants obtained with getMulitisInfo call + * @param threshold - number of required signers to make valid transaction. Must be equal to number of participants (N) or N - 1 + * @return in case of N / N wallets returns empty string since no more key exchanges needed. For N - 1 / N wallets returns base58 encoded extra multisig info + */ + virtual std::string makeMultisig(const std::vector& info, uint32_t threshold) = 0; + /** + * @brief finalizeMultisig - finalizes N - 1 / N multisig wallets creation + * @param extraMultisigInfo - wallet participants' extra multisig info obtained with makeMultisig call + * @return true if success + */ + virtual bool finalizeMultisig(const std::vector& extraMultisigInfo) = 0; + /** + * @brief exportMultisigImages - exports transfers' key images + * @param images - output paramter for hex encoded array of images + * @return true if success + */ + virtual bool exportMultisigImages(std::string& images) = 0; + /** + * @brief importMultisigImages - imports other participants' multisig images + * @param images - array of hex encoded arrays of images obtained with exportMultisigImages + * @return number of imported images + */ + virtual size_t importMultisigImages(const std::vector& images) = 0; + + /** + * @brief restoreMultisigTransaction creates PendingTransaction from signData + * @param signData encrypted unsigned transaction. Obtained with PendingTransaction::multisigSignData + * @return PendingTransaction + */ + virtual PendingTransaction* restoreMultisigTransaction(const std::string& signData) = 0; /*! * \brief createTransaction creates transaction. if dst_addr is an integrated address, payment_id is ignored * \param dst_addr destination address as string diff --git a/src/wallet/wallet2.cpp b/src/wallet/wallet2.cpp index 4b7e6dd93..e272620b9 100644 --- a/src/wallet/wallet2.cpp +++ b/src/wallet/wallet2.cpp @@ -4955,7 +4955,7 @@ bool wallet2::save_multisig_tx(const multisig_tx_set &txs, const std::string &fi return epee::file_io_utils::save_string_to_file(filename, ciphertext); } //---------------------------------------------------------------------------------------------------- -std::string wallet2::save_multisig_tx(const std::vector& ptx_vector) +wallet2::multisig_tx_set wallet2::make_multisig_tx_set(const std::vector& ptx_vector) const { multisig_tx_set txs; txs.m_ptx = ptx_vector; @@ -4967,8 +4967,12 @@ std::string wallet2::save_multisig_tx(const std::vector& ptx_vector) } txs.m_signers.insert(get_multisig_signer_public_key()); + return txs; +} - return save_multisig_tx(txs); +std::string wallet2::save_multisig_tx(const std::vector& ptx_vector) +{ + return save_multisig_tx(make_multisig_tx_set(ptx_vector)); } //---------------------------------------------------------------------------------------------------- bool wallet2::save_multisig_tx(const std::vector& ptx_vector, const std::string &filename) diff --git a/src/wallet/wallet2.h b/src/wallet/wallet2.h index abc7bb538..8cd6109f9 100644 --- a/src/wallet/wallet2.h +++ b/src/wallet/wallet2.h @@ -686,6 +686,7 @@ namespace tools bool save_multisig_tx(const multisig_tx_set &txs, const std::string &filename); std::string save_multisig_tx(const std::vector& ptx_vector); bool save_multisig_tx(const std::vector& ptx_vector, const std::string &filename); + multisig_tx_set make_multisig_tx_set(const std::vector& ptx_vector) const; // 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 &ptx, std::function accept_func = NULL, bool export_raw = false); // sign unsigned tx. Takes unsigned_tx_set as argument. Used by GUI