// Copyright (c) 2021, 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 <boost/multiprecision/cpp_int.hpp>
#include <algorithm>
#include <cstring>
#include <limits>
#include <set>
#include <string>
#include <unordered_map>
#include <unordered_set>
#include <vector>
#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<cryptonote::tx_source_entry>& 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<cryptonote::tx_source_entry>& sources,
const std::uint32_t subaddr_account,
const std::set<std::uint32_t>& 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<crypto::public_key, cryptonote::subaddress_index> 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<cryptonote::tx_destination_entry>& destinations
)
{
std::shuffle(destinations.begin(), destinations.end(), crypto::random_device{});
}
//----------------------------------------------------------------------------------------------------------------------
//----------------------------------------------------------------------------------------------------------------------
static bool set_tx_extra(
const cryptonote::account_keys& account_keys,
const std::vector<cryptonote::tx_destination_entry>& destinations,
const cryptonote::tx_destination_entry& change,
const crypto::secret_key& tx_secret_key,
const crypto::public_key& tx_public_key,
const std::vector<crypto::public_key>& tx_aux_public_keys,
const std::vector<std::uint8_t>& 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<cryptonote::tx_extra_field> 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<cryptonote::tx_source_entry>& 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<crypto::secret_key>& 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<crypto::secret_key>& tx_secret_keys,
crypto::secret_key& tx_secret_key,
std::vector<crypto::secret_key>& 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<cryptonote::tx_destination_entry>& destinations,
const cryptonote::tx_destination_entry& change,
const std::vector<std::uint8_t>& 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<crypto::secret_key>& tx_aux_secret_keys,
rct::keyV& output_public_keys,
rct::keyV& output_amount_secret_keys,
std::vector<crypto::view_tag>& 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<cryptonote::account_public_address> unique_subbaddr_recipients;
std::unordered_set<cryptonote::account_public_address> 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<crypto::secret_key> 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<crypto::secret_key> 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<crypto::public_key> 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<cryptonote::tx_source_entry>& 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<std::uint64_t> 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<crypto::view_tag>& 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<std::uint64_t>& 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<cryptonote::tx_source_entry>& sources,
const std::vector<cryptonote::tx_destination_entry>& 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_context_t>& 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<std::uint64_t> 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<rct::key *>(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<rct::key *>(&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<cryptonote::tx_source_entry>& sources,
const std::vector<cryptonote::tx_destination_entry>& 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<std::uint64_t>::max())
return false;
fee = static_cast<std::uint64_t>(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<rct::key *>(cached_w.data()), cached_w.size() * sizeof(rct::key));
}
//----------------------------------------------------------------------------------------------------------------------
bool tx_builder_ringct_t::init(
const cryptonote::account_keys& account_keys,
const std::vector<std::uint8_t>& extra,
const std::uint64_t unlock_time,
const std::uint32_t subaddr_account,
const std::set<std::uint32_t>& subaddr_minor_indices,
std::vector<cryptonote::tx_source_entry>& sources,
std::vector<cryptonote::tx_destination_entry>& 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<crypto::secret_key>& 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<rct::key *>(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<crypto::view_tag> view_tags;
auto output_amount_secret_keys_wiper = epee::misc_utils::create_scope_leave_handler([&]{
memwipe(static_cast<rct::key *>(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<rct::key *>(&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<rct::key *>(&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<rct::key *>(&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<cryptonote::tx_source_entry>& 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