// Copyright (c) 2021-2023, The Monero Project // // All rights reserved. // // Redistribution and use in source and binary forms, with or without modification, are // permitted provided that the following conditions are met: // // 1. Redistributions of source code must retain the above copyright notice, this list of // conditions and the following disclaimer. // // 2. Redistributions in binary form must reproduce the above copyright notice, this list // of conditions and the following disclaimer in the documentation and/or other // materials provided with the distribution. // // 3. Neither the name of the copyright holder nor the names of its contributors may be // used to endorse or promote products derived from this software without specific // prior written permission. // // THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY // EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF // MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL // THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, // SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, // PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS // INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, // STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF // THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. #include "multisig_tx_builder_ringct.h" #include "int-util.h" #include "memwipe.h" #include "cryptonote_basic/cryptonote_basic.h" #include "cryptonote_basic/account.h" #include "cryptonote_basic/cryptonote_format_utils.h" #include "cryptonote_config.h" #include "cryptonote_core/cryptonote_tx_utils.h" #include "device/device.hpp" #include "multisig_clsag_context.h" #include "ringct/bulletproofs.h" #include "ringct/bulletproofs_plus.h" #include "ringct/rctSigs.h" #include #include #include #include #include #include #include #include #include #undef MONERO_DEFAULT_LOG_CATEGORY #define MONERO_DEFAULT_LOG_CATEGORY "multisig" namespace multisig { namespace signing { //---------------------------------------------------------------------------------------------------------------------- //---------------------------------------------------------------------------------------------------------------------- bool view_tag_required(const int bp_version) { // view tags were introduced at the same time as BP+, so they are needed after BP+ (v4 and later) if (bp_version <= 3) return false; else return true; } //---------------------------------------------------------------------------------------------------------------------- //---------------------------------------------------------------------------------------------------------------------- static void sort_sources( std::vector& sources ) { std::sort(sources.begin(), sources.end(), [](const auto& lhs, const auto& rhs){ const rct::key& ki0 = lhs.multisig_kLRki.ki; const rct::key& ki1 = rhs.multisig_kLRki.ki; return memcmp(&ki0, &ki1, sizeof(rct::key)) > 0; }); } //---------------------------------------------------------------------------------------------------------------------- //---------------------------------------------------------------------------------------------------------------------- static bool compute_keys_for_sources( const cryptonote::account_keys& account_keys, const std::vector& sources, const std::uint32_t subaddr_account, const std::set& subaddr_minor_indices, rct::keyV& input_secret_keys ) { const std::size_t num_sources = sources.size(); hw::device& hwdev = account_keys.get_device(); std::unordered_map subaddresses; for (const std::uint32_t minor_index: subaddr_minor_indices) { subaddresses[hwdev.get_subaddress_spend_public_key( account_keys, {subaddr_account, minor_index} )] = {subaddr_account, minor_index}; } input_secret_keys.resize(num_sources); for (std::size_t i = 0; i < num_sources; ++i) { const auto& src = sources[i]; crypto::key_image tmp_key_image; cryptonote::keypair tmp_keys; if (src.real_output >= src.outputs.size()) return false; if (not cryptonote::generate_key_image_helper( account_keys, subaddresses, rct::rct2pk(src.outputs[src.real_output].second.dest), src.real_out_tx_key, src.real_out_additional_tx_keys, src.real_output_in_tx_index, tmp_keys, tmp_key_image, hwdev )) { return false; } input_secret_keys[i] = rct::sk2rct(tmp_keys.sec); } return true; } //---------------------------------------------------------------------------------------------------------------------- //---------------------------------------------------------------------------------------------------------------------- static void shuffle_destinations( std::vector& destinations ) { std::shuffle(destinations.begin(), destinations.end(), crypto::random_device{}); } //---------------------------------------------------------------------------------------------------------------------- //---------------------------------------------------------------------------------------------------------------------- static bool set_tx_extra( const cryptonote::account_keys& account_keys, const std::vector& destinations, const cryptonote::tx_destination_entry& change, const crypto::secret_key& tx_secret_key, const crypto::public_key& tx_public_key, const std::vector& tx_aux_public_keys, const std::vector& extra, cryptonote::transaction& tx ) { hw::device &hwdev = account_keys.get_device(); tx.extra = extra; // if we have a stealth payment id, find it and encrypt it with the tx key now std::vector tx_extra_fields; if (cryptonote::parse_tx_extra(tx.extra, tx_extra_fields)) { bool add_dummy_payment_id = true; cryptonote::tx_extra_nonce extra_nonce; if (cryptonote::find_tx_extra_field_by_type(tx_extra_fields, extra_nonce)) { crypto::hash payment_id = crypto::null_hash; crypto::hash8 payment_id8 = crypto::null_hash8; if (cryptonote::get_encrypted_payment_id_from_tx_extra_nonce(extra_nonce.nonce, payment_id8)) { LOG_PRINT_L2("Encrypting payment id " << payment_id8); crypto::public_key view_key_pub = cryptonote::get_destination_view_key_pub(destinations, change.addr); if (view_key_pub == crypto::null_pkey) { // valid combinations: // - 1 output with encrypted payment ID, dummy change output (0 amount) // - 0 outputs, 1 change output with encrypted payment ID // - 1 output with encrypted payment ID, 1 change output LOG_ERROR("Destinations have to have exactly one output to support encrypted payment ids"); return false; } if (!hwdev.encrypt_payment_id(payment_id8, view_key_pub, tx_secret_key)) { LOG_ERROR("Failed to encrypt payment id"); return false; } std::string extra_nonce_updated; cryptonote::set_encrypted_payment_id_to_tx_extra_nonce(extra_nonce_updated, payment_id8); cryptonote::remove_field_from_tx_extra(tx.extra, typeid(cryptonote::tx_extra_nonce)); if (!cryptonote::add_extra_nonce_to_tx_extra(tx.extra, extra_nonce_updated)) { LOG_ERROR("Failed to add encrypted payment id to tx extra"); return false; } LOG_PRINT_L1("Encrypted payment ID: " << payment_id8); add_dummy_payment_id = false; } else if (cryptonote::get_payment_id_from_tx_extra_nonce(extra_nonce.nonce, payment_id)) { add_dummy_payment_id = false; } } // we don't add one if we've got more than the usual 1 destination plus change if (destinations.size() > 2) add_dummy_payment_id = false; if (add_dummy_payment_id) { // if we have neither long nor short payment id, add a dummy short one, // this should end up being the vast majority of txes as time goes on std::string extra_nonce_updated; crypto::hash8 payment_id8 = crypto::null_hash8; crypto::public_key view_key_pub = cryptonote::get_destination_view_key_pub(destinations, change.addr); if (view_key_pub == crypto::null_pkey) { LOG_ERROR("Failed to get key to encrypt dummy payment id with"); } else { hwdev.encrypt_payment_id(payment_id8, view_key_pub, tx_secret_key); cryptonote::set_encrypted_payment_id_to_tx_extra_nonce(extra_nonce_updated, payment_id8); if (!cryptonote::add_extra_nonce_to_tx_extra(tx.extra, extra_nonce_updated)) { LOG_ERROR("Failed to add dummy encrypted payment id to tx extra"); // continue anyway } } } } else { MWARNING("Failed to parse tx extra"); tx_extra_fields.clear(); } cryptonote::remove_field_from_tx_extra(tx.extra, typeid(cryptonote::tx_extra_pub_key)); cryptonote::add_tx_pub_key_to_extra(tx.extra, tx_public_key); cryptonote::remove_field_from_tx_extra(tx.extra, typeid(cryptonote::tx_extra_additional_pub_keys)); LOG_PRINT_L2("tx pubkey: " << tx_public_key); if (tx_aux_public_keys.size()) { LOG_PRINT_L2("additional tx pubkeys: "); for (size_t i = 0; i < tx_aux_public_keys.size(); ++i) LOG_PRINT_L2(tx_aux_public_keys[i]); cryptonote::add_additional_tx_pub_keys_to_extra(tx.extra, tx_aux_public_keys); } if (not cryptonote::sort_tx_extra(tx.extra, tx.extra)) return false; return true; } //---------------------------------------------------------------------------------------------------------------------- //---------------------------------------------------------------------------------------------------------------------- static void make_tx_secret_key_seed(const crypto::secret_key& tx_secret_key_entropy, const std::vector& sources, crypto::secret_key& tx_secret_key_seed) { // seed = H(H("domain separator"), entropy, {KI}) static const std::string domain_separator{config::HASH_KEY_MULTISIG_TX_PRIVKEYS_SEED}; rct::keyV hash_context; hash_context.reserve(2 + sources.size()); auto hash_context_wiper = epee::misc_utils::create_scope_leave_handler([&]{ memwipe(hash_context.data(), hash_context.size()); }); hash_context.emplace_back(); rct::cn_fast_hash(hash_context.back(), domain_separator.data(), domain_separator.size()); //domain sep hash_context.emplace_back(rct::sk2rct(tx_secret_key_entropy)); //entropy for (const cryptonote::tx_source_entry& source : sources) hash_context.emplace_back(source.multisig_kLRki.ki); //{KI} // set the seed tx_secret_key_seed = rct::rct2sk(rct::cn_fast_hash(hash_context)); } //---------------------------------------------------------------------------------------------------------------------- //---------------------------------------------------------------------------------------------------------------------- static void make_tx_secret_keys(const crypto::secret_key& tx_secret_key_seed, const std::size_t num_tx_keys, std::vector& tx_secret_keys) { // make tx secret keys as a hash chain of the seed // h1 = H_n(seed || H("domain separator")) // h2 = H_n(seed || h1) // h3 = H_n(seed || h2) // ... static const std::string domain_separator{config::HASH_KEY_MULTISIG_TX_PRIVKEYS}; rct::keyV hash_context; hash_context.resize(2); auto hash_context_wiper = epee::misc_utils::create_scope_leave_handler([&]{ memwipe(hash_context.data(), hash_context.size()); }); hash_context[0] = rct::sk2rct(tx_secret_key_seed); rct::cn_fast_hash(hash_context[1], domain_separator.data(), domain_separator.size()); tx_secret_keys.clear(); tx_secret_keys.resize(num_tx_keys); for (crypto::secret_key& tx_secret_key : tx_secret_keys) { // advance the hash chain hash_context[1] = rct::hash_to_scalar(hash_context); // set this key tx_secret_key = rct::rct2sk(hash_context[1]); } } //---------------------------------------------------------------------------------------------------------------------- //---------------------------------------------------------------------------------------------------------------------- static bool collect_tx_secret_keys(const std::vector& tx_secret_keys, crypto::secret_key& tx_secret_key, std::vector& tx_aux_secret_keys) { if (tx_secret_keys.size() == 0) return false; tx_secret_key = tx_secret_keys[0]; tx_aux_secret_keys.clear(); tx_aux_secret_keys.reserve(tx_secret_keys.size() - 1); for (std::size_t tx_key_index{1}; tx_key_index < tx_secret_keys.size(); ++tx_key_index) tx_aux_secret_keys.emplace_back(tx_secret_keys[tx_key_index]); return true; } //---------------------------------------------------------------------------------------------------------------------- //---------------------------------------------------------------------------------------------------------------------- static bool compute_keys_for_destinations( const cryptonote::account_keys& account_keys, const std::uint32_t subaddr_account, const std::vector& destinations, const cryptonote::tx_destination_entry& change, const std::vector& extra, const bool use_view_tags, const bool reconstruction, const crypto::secret_key& tx_secret_key_seed, crypto::secret_key& tx_secret_key, std::vector& tx_aux_secret_keys, rct::keyV& output_public_keys, rct::keyV& output_amount_secret_keys, std::vector& view_tags, cryptonote::transaction& unsigned_tx ) { hw::device &hwdev = account_keys.get_device(); // check non-zero change amount case if (change.amount > 0) { // the change output must be directed to the local account if (change.addr != hwdev.get_subaddress(account_keys, {subaddr_account})) return false; // expect the change destination to be in the destination set if (std::find_if(destinations.begin(), destinations.end(), [&change](const auto &destination) -> bool { return destination.addr == change.addr; }) == destinations.end()) return false; } // collect non-change recipients into normal/subaddress buckets std::unordered_set unique_subbaddr_recipients; std::unordered_set unique_std_recipients; for(const auto& dst_entr: destinations) { if (dst_entr.addr == change.addr) continue; if (dst_entr.is_subaddress) unique_subbaddr_recipients.insert(dst_entr.addr); else unique_std_recipients.insert(dst_entr.addr); } // figure out how many tx secret keys are needed // - tx aux keys: add if there are > 1 non-change recipients, with at least one to a subaddress const std::size_t num_destinations = destinations.size(); const bool need_tx_aux_keys = unique_subbaddr_recipients.size() + bool(unique_std_recipients.size()) > 1; const std::size_t num_tx_keys = 1 + (need_tx_aux_keys ? num_destinations : 0); // make tx secret keys std::vector all_tx_secret_keys; make_tx_secret_keys(tx_secret_key_seed, num_tx_keys, all_tx_secret_keys); // split up tx secret keys crypto::secret_key tx_secret_key_temp; std::vector tx_aux_secret_keys_temp; if (not collect_tx_secret_keys(all_tx_secret_keys, tx_secret_key_temp, tx_aux_secret_keys_temp)) return false; if (reconstruction) { // when reconstructing, the tx secret keys should be reproducible from input seed if (!(tx_secret_key == tx_secret_key_temp)) return false; if (!(tx_aux_secret_keys == tx_aux_secret_keys_temp)) return false; } else { tx_secret_key = tx_secret_key_temp; tx_aux_secret_keys = std::move(tx_aux_secret_keys_temp); } // tx pub key: R crypto::public_key tx_public_key; if (unique_std_recipients.empty() && unique_subbaddr_recipients.size() == 1) { // if there is exactly 1 non-change recipient, and it's to a subaddress, then the tx pubkey = r*Ksi_nonchange_recipient tx_public_key = rct::rct2pk( hwdev.scalarmultKey( rct::pk2rct(unique_subbaddr_recipients.begin()->m_spend_public_key), rct::sk2rct(tx_secret_key) )); } else { // otherwise, the tx pub key = r*G // - if there are > 1 non-change recipients, with at least one to a subaddress, then the tx pubkey is not used // (additional tx keys will be used instead) // - if all non-change recipients are to normal addresses, then the tx pubkey will be used by all recipients // (including change recipient, even if change is to a subaddress) tx_public_key = rct::rct2pk(hwdev.scalarmultBase(rct::sk2rct(tx_secret_key))); } // additional tx pubkeys: R_t output_public_keys.resize(num_destinations); view_tags.resize(num_destinations); std::vector tx_aux_public_keys; crypto::public_key temp_output_public_key; for (std::size_t i = 0; i < num_destinations; ++i) { if (not hwdev.generate_output_ephemeral_keys( unsigned_tx.version, account_keys, tx_public_key, tx_secret_key, destinations[i], change.addr, i, need_tx_aux_keys, tx_aux_secret_keys, tx_aux_public_keys, output_amount_secret_keys, temp_output_public_key, use_view_tags, view_tags[i] //unused variable if use_view_tags is not set )) { return false; } output_public_keys[i] = rct::pk2rct(temp_output_public_key); } if (num_destinations != output_amount_secret_keys.size()) return false; CHECK_AND_ASSERT_MES( tx_aux_public_keys.size() == tx_aux_secret_keys.size(), false, "Internal error creating additional public keys" ); if (not set_tx_extra(account_keys, destinations, change, tx_secret_key, tx_public_key, tx_aux_public_keys, extra, unsigned_tx)) return false; return true; } //---------------------------------------------------------------------------------------------------------------------- //---------------------------------------------------------------------------------------------------------------------- static void set_tx_inputs( const std::vector& sources, cryptonote::transaction& unsigned_tx ) { const std::size_t num_sources = sources.size(); unsigned_tx.vin.resize(num_sources); for (std::size_t i = 0; i < num_sources; ++i) { std::vector offsets; offsets.reserve(sources[i].outputs.size()); for (const auto& e: sources[i].outputs) offsets.emplace_back(e.first); unsigned_tx.vin[i] = cryptonote::txin_to_key{ .amount = 0, .key_offsets = cryptonote::absolute_output_offsets_to_relative(offsets), .k_image = rct::rct2ki(sources[i].multisig_kLRki.ki), }; } } //---------------------------------------------------------------------------------------------------------------------- //---------------------------------------------------------------------------------------------------------------------- static bool onetime_addresses_are_unique(const rct::keyV& output_public_keys) { for (auto addr_it = output_public_keys.begin(); addr_it != output_public_keys.end(); ++addr_it) { if (std::find(output_public_keys.begin(), addr_it, *addr_it) != addr_it) return false; } return true; } //---------------------------------------------------------------------------------------------------------------------- //---------------------------------------------------------------------------------------------------------------------- static bool set_tx_outputs(const rct::keyV& output_public_keys, cryptonote::transaction& unsigned_tx) { // sanity check: all onetime addresses should be unique if (not onetime_addresses_are_unique(output_public_keys)) return false; // set the tx outputs const std::size_t num_destinations = output_public_keys.size(); unsigned_tx.vout.resize(num_destinations); for (std::size_t i = 0; i < num_destinations; ++i) cryptonote::set_tx_out(0, rct::rct2pk(output_public_keys[i]), false, crypto::view_tag{}, unsigned_tx.vout[i]); return true; } //---------------------------------------------------------------------------------------------------------------------- //---------------------------------------------------------------------------------------------------------------------- static bool set_tx_outputs_with_view_tags( const rct::keyV& output_public_keys, const std::vector& view_tags, cryptonote::transaction& unsigned_tx ) { // sanity check: all onetime addresses should be unique if (not onetime_addresses_are_unique(output_public_keys)) return false; // set the tx outputs (with view tags) const std::size_t num_destinations = output_public_keys.size(); CHECK_AND_ASSERT_MES(view_tags.size() == num_destinations, false, "multisig signing protocol: internal error, view tag size mismatch."); unsigned_tx.vout.resize(num_destinations); for (std::size_t i = 0; i < num_destinations; ++i) cryptonote::set_tx_out(0, rct::rct2pk(output_public_keys[i]), true, view_tags[i], unsigned_tx.vout[i]); return true; } //---------------------------------------------------------------------------------------------------------------------- //---------------------------------------------------------------------------------------------------------------------- static void make_new_range_proofs(const int bp_version, const std::vector& output_amounts, const rct::keyV& output_amount_masks, rct::rctSigPrunable& sigs) { sigs.bulletproofs.clear(); sigs.bulletproofs_plus.clear(); if (bp_version == 3) sigs.bulletproofs.push_back(rct::bulletproof_PROVE(output_amounts, output_amount_masks)); else if (bp_version == 4) sigs.bulletproofs_plus.push_back(rct::bulletproof_plus_PROVE(output_amounts, output_amount_masks)); } //---------------------------------------------------------------------------------------------------------------------- //---------------------------------------------------------------------------------------------------------------------- static bool try_reconstruct_range_proofs(const int bp_version, const rct::rctSigPrunable& original_sigs, const std::size_t num_destinations, const rct::ctkeyV& output_public_keys, rct::rctSigPrunable& reconstructed_sigs) { auto try_reconstruct_range_proofs = [&](const auto &original_range_proofs, auto &new_range_proofs) -> bool { if (original_range_proofs.size() != 1) return false; new_range_proofs = original_range_proofs; new_range_proofs[0].V.resize(num_destinations); for (std::size_t i = 0; i < num_destinations; ++i) new_range_proofs[0].V[i] = rct::scalarmultKey(output_public_keys[i].mask, rct::INV_EIGHT); return true; }; if (bp_version == 3) { if (not try_reconstruct_range_proofs(original_sigs.bulletproofs, reconstructed_sigs.bulletproofs)) return false; return rct::bulletproof_VERIFY(reconstructed_sigs.bulletproofs); } else if (bp_version == 4) { if (not try_reconstruct_range_proofs(original_sigs.bulletproofs_plus, reconstructed_sigs.bulletproofs_plus)) return false; return rct::bulletproof_plus_VERIFY(reconstructed_sigs.bulletproofs_plus); } return false; } //---------------------------------------------------------------------------------------------------------------------- //---------------------------------------------------------------------------------------------------------------------- static bool set_tx_rct_signatures( const std::uint64_t fee, const std::vector& sources, const std::vector& destinations, const rct::keyV& input_secret_keys, const rct::keyV& output_public_keys, const rct::keyV& output_amount_secret_keys, const rct::RCTConfig& rct_config, const bool reconstruction, cryptonote::transaction& unsigned_tx, std::vector& CLSAG_contexts, rct::keyV& cached_w ) { if (rct_config.bp_version != 3 && rct_config.bp_version != 4) return false; if (rct_config.range_proof_type != rct::RangeProofPaddedBulletproof) return false; const std::size_t num_destinations = destinations.size(); const std::size_t num_sources = sources.size(); // rct_signatures component of tx rct::rctSig rv{}; // set misc. fields if (rct_config.bp_version == 3) rv.type = rct::RCTTypeCLSAG; else if (rct_config.bp_version == 4) rv.type = rct::RCTTypeBulletproofPlus; else return false; rv.txnFee = fee; rv.message = rct::hash2rct(cryptonote::get_transaction_prefix_hash(unsigned_tx)); // define outputs std::vector output_amounts(num_destinations); rct::keyV output_amount_masks(num_destinations); rv.ecdhInfo.resize(num_destinations); rv.outPk.resize(num_destinations); for (std::size_t i = 0; i < num_destinations; ++i) { rv.outPk[i].dest = output_public_keys[i]; output_amounts[i] = destinations[i].amount; output_amount_masks[i] = genCommitmentMask(output_amount_secret_keys[i]); rv.ecdhInfo[i].amount = rct::d2h(output_amounts[i]); rct::addKeys2( rv.outPk[i].mask, output_amount_masks[i], rv.ecdhInfo[i].amount, rct::H ); rct::ecdhEncode(rv.ecdhInfo[i], output_amount_secret_keys[i], true); } // output range proofs if (not reconstruction) { make_new_range_proofs(rct_config.bp_version, output_amounts, output_amount_masks, rv.p); } else { if (not try_reconstruct_range_proofs(rct_config.bp_version, unsigned_tx.rct_signatures.p, num_destinations, rv.outPk, rv.p)) return false; } // prepare rings for input CLSAGs rv.mixRing.resize(num_sources); for (std::size_t i = 0; i < num_sources; ++i) { const std::size_t ring_size = sources[i].outputs.size(); rv.mixRing[i].resize(ring_size); for (std::size_t j = 0; j < ring_size; ++j) { rv.mixRing[i][j].dest = sources[i].outputs[j].second.dest; rv.mixRing[i][j].mask = sources[i].outputs[j].second.mask; } } // make pseudo-output commitments rct::keyV a; //pseudo-output commitment blinding factors auto a_wiper = epee::misc_utils::create_scope_leave_handler([&]{ memwipe(static_cast(a.data()), a.size() * sizeof(rct::key)); }); if (not reconstruction) { a.resize(num_sources); rv.p.pseudoOuts.resize(num_sources); a[num_sources - 1] = rct::zero(); for (std::size_t i = 0; i < num_destinations; ++i) { sc_add( a[num_sources - 1].bytes, a[num_sources - 1].bytes, output_amount_masks[i].bytes ); } for (std::size_t i = 0; i < num_sources - 1; ++i) { rct::skGen(a[i]); sc_sub( a[num_sources - 1].bytes, a[num_sources - 1].bytes, a[i].bytes ); rct::genC(rv.p.pseudoOuts[i], a[i], sources[i].amount); } rct::genC( rv.p.pseudoOuts[num_sources - 1], a[num_sources - 1], sources[num_sources - 1].amount ); } // check balance if reconstructing the tx else { rv.p.pseudoOuts = unsigned_tx.rct_signatures.p.pseudoOuts; if (num_sources != rv.p.pseudoOuts.size()) return false; rct::key balance_accumulator = rct::scalarmultH(rct::d2h(fee)); for (const auto& e: rv.outPk) rct::addKeys(balance_accumulator, balance_accumulator, e.mask); for (const auto& pseudoOut: rv.p.pseudoOuts) rct::subKeys(balance_accumulator, balance_accumulator, pseudoOut); if (not (balance_accumulator == rct::identity())) return false; } // prepare input CLSAGs for signing const rct::key message = get_pre_mlsag_hash(rv, hw::get_device("default")); rv.p.CLSAGs.resize(num_sources); if (reconstruction) { if (num_sources != unsigned_tx.rct_signatures.p.CLSAGs.size()) return false; } CLSAG_contexts.resize(num_sources); if (not reconstruction) cached_w.resize(num_sources); for (std::size_t i = 0; i < num_sources; ++i) { const std::size_t ring_size = rv.mixRing[i].size(); const rct::key& I = sources[i].multisig_kLRki.ki; const std::size_t l = sources[i].real_output; if (l >= ring_size) return false; rct::keyV& s = rv.p.CLSAGs[i].s; const rct::key& C_offset = rv.p.pseudoOuts[i]; rct::keyV P(ring_size); rct::keyV C_nonzero(ring_size); if (not reconstruction) { s.resize(ring_size); for (std::size_t j = 0; j < ring_size; ++j) { if (j != l) s[j] = rct::skGen(); //make fake responses } } else { if (ring_size != unsigned_tx.rct_signatures.p.CLSAGs[i].s.size()) return false; s = unsigned_tx.rct_signatures.p.CLSAGs[i].s; } for (std::size_t j = 0; j < ring_size; ++j) { P[j] = rv.mixRing[i][j].dest; C_nonzero[j] = rv.mixRing[i][j].mask; } rct::key D; rct::key z; auto z_wiper = epee::misc_utils::create_scope_leave_handler([&]{ memwipe(static_cast(&z), sizeof(rct::key)); }); if (not reconstruction) { sc_sub(z.bytes, sources[i].mask.bytes, a[i].bytes); //commitment to zero privkey ge_p3 H_p3; rct::hash_to_p3(H_p3, rv.mixRing[i][l].dest); rct::key H_l; ge_p3_tobytes(H_l.bytes, &H_p3); D = rct::scalarmultKey(H_l, z); //auxilliary key image (for commitment to zero) rv.p.CLSAGs[i].D = rct::scalarmultKey(D, rct::INV_EIGHT); rv.p.CLSAGs[i].I = I; } else { rv.p.CLSAGs[i].D = unsigned_tx.rct_signatures.p.CLSAGs[i].D; rv.p.CLSAGs[i].I = I; D = rct::scalarmultKey(rv.p.CLSAGs[i].D, rct::EIGHT); } if (not CLSAG_contexts[i].init(P, C_nonzero, C_offset, message, I, D, l, s, kAlphaComponents)) return false; if (not reconstruction) { rct::key mu_P; rct::key mu_C; if (not CLSAG_contexts[i].get_mu(mu_P, mu_C)) return false; sc_mul(cached_w[i].bytes, mu_P.bytes, input_secret_keys[i].bytes); sc_muladd(cached_w[i].bytes, mu_C.bytes, z.bytes, cached_w[i].bytes); } } unsigned_tx.rct_signatures = std::move(rv); return true; } //---------------------------------------------------------------------------------------------------------------------- //---------------------------------------------------------------------------------------------------------------------- static bool compute_tx_fee( const std::vector& sources, const std::vector& destinations, std::uint64_t& fee ) { boost::multiprecision::uint128_t in_amount = 0; for (const auto& src: sources) in_amount += src.amount; boost::multiprecision::uint128_t out_amount = 0; for (const auto& dst: destinations) out_amount += dst.amount; if (out_amount > in_amount) return false; if (in_amount - out_amount > std::numeric_limits::max()) return false; fee = static_cast(in_amount - out_amount); return true; } //---------------------------------------------------------------------------------------------------------------------- tx_builder_ringct_t::tx_builder_ringct_t(): initialized(false) {} //---------------------------------------------------------------------------------------------------------------------- tx_builder_ringct_t::~tx_builder_ringct_t() { memwipe(static_cast(cached_w.data()), cached_w.size() * sizeof(rct::key)); } //---------------------------------------------------------------------------------------------------------------------- bool tx_builder_ringct_t::init( const cryptonote::account_keys& account_keys, const std::vector& extra, const std::uint64_t unlock_time, const std::uint32_t subaddr_account, const std::set& subaddr_minor_indices, std::vector& sources, std::vector& destinations, const cryptonote::tx_destination_entry& change, const rct::RCTConfig& rct_config, const bool use_rct, const bool reconstruction, crypto::secret_key& tx_secret_key, std::vector& tx_aux_secret_keys, crypto::secret_key& tx_secret_key_entropy, cryptonote::transaction& unsigned_tx ) { initialized = false; this->reconstruction = reconstruction; if (not use_rct) return false; if (sources.empty()) return false; if (not reconstruction) unsigned_tx.set_null(); std::uint64_t fee; if (not compute_tx_fee(sources, destinations, fee)) return false; // decide if view tags are needed const bool use_view_tags{view_tag_required(rct_config.bp_version)}; // misc. fields unsigned_tx.version = 2; //rct = 2 unsigned_tx.unlock_time = unlock_time; // sort inputs sort_sources(sources); // prepare tx secret key seed (must be AFTER sorting sources) // - deriving the seed from sources plus entropy ensures uniqueness for every new tx attempt // - the goal is that two multisig txs added to the chain will never have outputs with the same onetime addresses, // which would burn funds (embedding the inputs' key images guarantees this) // - it is acceptable if two tx attempts use the same input set and entropy (only a malicious tx proposer will do // that, but all it can accomplish is leaking information about the recipients - which a malicious proposer can // easily do outside the signing ritual anyway) if (not reconstruction) tx_secret_key_entropy = rct::rct2sk(rct::skGen()); // expect not null (note: wallet serialization code may set this to null if handling an old partial tx) if (tx_secret_key_entropy == crypto::null_skey) return false; crypto::secret_key tx_secret_key_seed; make_tx_secret_key_seed(tx_secret_key_entropy, sources, tx_secret_key_seed); // get secret keys for signing input CLSAGs (multisig: or for the initial partial signature) rct::keyV input_secret_keys; auto input_secret_keys_wiper = epee::misc_utils::create_scope_leave_handler([&]{ memwipe(static_cast(input_secret_keys.data()), input_secret_keys.size() * sizeof(rct::key)); }); if (not compute_keys_for_sources(account_keys, sources, subaddr_account, subaddr_minor_indices, input_secret_keys)) return false; // randomize output order if (not reconstruction) shuffle_destinations(destinations); // prepare outputs rct::keyV output_public_keys; rct::keyV output_amount_secret_keys; std::vector view_tags; auto output_amount_secret_keys_wiper = epee::misc_utils::create_scope_leave_handler([&]{ memwipe(static_cast(output_amount_secret_keys.data()), output_amount_secret_keys.size() * sizeof(rct::key)); }); if (not compute_keys_for_destinations(account_keys, subaddr_account, destinations, change, extra, use_view_tags, reconstruction, tx_secret_key_seed, tx_secret_key, tx_aux_secret_keys, output_public_keys, output_amount_secret_keys, view_tags, unsigned_tx)) return false; // add inputs to tx set_tx_inputs(sources, unsigned_tx); // add output one-time addresses to tx bool set_tx_outputs_result{false}; if (use_view_tags) set_tx_outputs_result = set_tx_outputs_with_view_tags(output_public_keys, view_tags, unsigned_tx); else set_tx_outputs_result = set_tx_outputs(output_public_keys, unsigned_tx); if (not set_tx_outputs_result) return false; // prepare input signatures if (not set_tx_rct_signatures(fee, sources, destinations, input_secret_keys, output_public_keys, output_amount_secret_keys, rct_config, reconstruction, unsigned_tx, CLSAG_contexts, cached_w)) return false; initialized = true; return true; } //---------------------------------------------------------------------------------------------------------------------- bool tx_builder_ringct_t::first_partial_sign( const std::size_t source, const rct::keyV& total_alpha_G, const rct::keyV& total_alpha_H, const rct::keyV& alpha, rct::key& c_0, rct::key& s ) { if (not initialized or reconstruction) return false; const std::size_t num_sources = CLSAG_contexts.size(); if (source >= num_sources) return false; rct::key c; rct::key alpha_combined; auto alpha_combined_wiper = epee::misc_utils::create_scope_leave_handler([&]{ memwipe(static_cast(&alpha_combined), sizeof(rct::key)); }); if (not CLSAG_contexts[source].combine_alpha_and_compute_challenge( total_alpha_G, total_alpha_H, alpha, alpha_combined, c_0, c )) { return false; } // initial partial response: // s = alpha_combined_local - challenge*[mu_P*(local keys and sender-receiver secret and subaddress material) + // mu_C*(commitment-to-zero secret)] sc_mulsub(s.bytes, c.bytes, cached_w[source].bytes, alpha_combined.bytes); return true; } //---------------------------------------------------------------------------------------------------------------------- bool tx_builder_ringct_t::next_partial_sign( const rct::keyM& total_alpha_G, const rct::keyM& total_alpha_H, const rct::keyM& alpha, const rct::key& x, rct::keyV& c_0, rct::keyV& s ) { if (not initialized or not reconstruction) return false; const std::size_t num_sources = CLSAG_contexts.size(); if (num_sources != total_alpha_G.size()) return false; if (num_sources != total_alpha_H.size()) return false; if (num_sources != alpha.size()) return false; if (num_sources != c_0.size()) return false; if (num_sources != s.size()) return false; for (std::size_t i = 0; i < num_sources; ++i) { rct::key c; rct::key alpha_combined; auto alpha_combined_wiper = epee::misc_utils::create_scope_leave_handler([&]{ memwipe(static_cast(&alpha_combined), sizeof(rct::key)); }); if (not CLSAG_contexts[i].combine_alpha_and_compute_challenge( total_alpha_G[i], total_alpha_H[i], alpha[i], alpha_combined, c_0[i], c )) { return false; } rct::key mu_P; rct::key mu_C; if (not CLSAG_contexts[i].get_mu(mu_P, mu_C)) return false; rct::key w; auto w_wiper = epee::misc_utils::create_scope_leave_handler([&]{ memwipe(static_cast(&w), sizeof(rct::key)); }); sc_mul(w.bytes, mu_P.bytes, x.bytes); // include local signer's response: // s += alpha_combined_local - challenge*[mu_P*(local keys)] sc_add(s[i].bytes, s[i].bytes, alpha_combined.bytes); sc_mulsub(s[i].bytes, c.bytes, w.bytes, s[i].bytes); } return true; } //---------------------------------------------------------------------------------------------------------------------- bool tx_builder_ringct_t::finalize_tx( const std::vector& sources, const rct::keyV& c_0, const rct::keyV& s, cryptonote::transaction& unsigned_tx ) { // checks const std::size_t num_sources = sources.size(); if (num_sources != unsigned_tx.rct_signatures.p.CLSAGs.size()) return false; if (num_sources != c_0.size()) return false; if (num_sources != s.size()) return false; // finalize tx signatures for (std::size_t i = 0; i < num_sources; ++i) { const std::size_t ring_size = unsigned_tx.rct_signatures.p.CLSAGs[i].s.size(); if (sources[i].real_output >= ring_size) return false; unsigned_tx.rct_signatures.p.CLSAGs[i].s[sources[i].real_output] = s[i]; unsigned_tx.rct_signatures.p.CLSAGs[i].c1 = c_0[i]; } return true; } //---------------------------------------------------------------------------------------------------------------------- } //namespace signing } //namespace multisig