You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
mymonero-core-cpp/src/monero_transfer_utils.cpp

834 lines
32 KiB

//
// monero_transfer_utils.cpp
// Copyright © 2018 MyMonero. All rights reserved.
//
// 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 "monero_transfer_utils.hpp"
#include "wallet_errors.h"
#include "string_tools.h"
#include "monero_paymentID_utils.hpp"
#include "monero_key_image_utils.hpp"
//
using namespace std;
using namespace crypto;
using namespace std;
using namespace boost;
using namespace epee;
using namespace cryptonote;
using namespace tools; // for error::
using namespace monero_transfer_utils;
using namespace monero_fork_rules;
using namespace monero_fee_utils;
using namespace monero_key_image_utils; // for API response parsing
//
// Transfer parsing/derived properties
bool monero_transfer_utils::is_transfer_unlocked(
uint64_t unlock_time,
uint64_t block_height,
uint64_t blockchain_size, /* extracting wallet2->m_blockchain.size() / m_local_bc_height */
network_type nettype
) {
if(!is_tx_spendtime_unlocked(unlock_time, block_height, blockchain_size, nettype))
return false;
if(block_height + CRYPTONOTE_DEFAULT_TX_SPENDABLE_AGE > blockchain_size)
return false;
return true;
}
bool monero_transfer_utils::is_tx_spendtime_unlocked(
uint64_t unlock_time,
uint64_t block_height,
uint64_t blockchain_size,
network_type nettype
) {
if(unlock_time < CRYPTONOTE_MAX_BLOCK_NUMBER)
{
//interpret as block index
if(blockchain_size-1 + CRYPTONOTE_LOCKED_TX_ALLOWED_DELTA_BLOCKS >= unlock_time)
return true;
else
return false;
}else
{
//interpret as time
uint64_t current_time = static_cast<uint64_t>(time(NULL));
// XXX: this needs to be fast, so we'd need to get the starting heights
// from the daemon to be correct once voting kicks in
uint64_t v2height = nettype == TESTNET ? 624634 : nettype == STAGENET ? (uint64_t)-1/*TODO*/ : 1009827;
uint64_t leeway = block_height < v2height ? CRYPTONOTE_LOCKED_TX_ALLOWED_DELTA_SECONDS_V1 : CRYPTONOTE_LOCKED_TX_ALLOWED_DELTA_SECONDS_V2;
if(current_time + leeway >= unlock_time)
return true;
else
return false;
}
return false;
}
//
namespace {
CreateTransactionErrorCode _add_pid_to_tx_extra(
const optional<string>& payment_id_string,
vector<uint8_t> &extra
) { // Detect hash8 or hash32 char hex string as pid and configure 'extra' accordingly
bool r = false;
if (payment_id_string != none && payment_id_string->size() > 0) {
crypto::hash payment_id;
r = monero_paymentID_utils::parse_long_payment_id(*payment_id_string, payment_id);
if (r) {
std::string extra_nonce;
cryptonote::set_payment_id_to_tx_extra_nonce(extra_nonce, payment_id);
r = cryptonote::add_extra_nonce_to_tx_extra(extra, extra_nonce);
if (!r) {
return couldntAddPIDNonceToTXExtra;
}
} else {
crypto::hash8 payment_id8;
r = monero_paymentID_utils::parse_short_payment_id(*payment_id_string, payment_id8);
if (!r) { // a PID has been specified by the user but the last resort in validating it fails; error
return invalidPID;
}
std::string extra_nonce;
cryptonote::set_encrypted_payment_id_to_tx_extra_nonce(extra_nonce, payment_id8);
r = cryptonote::add_extra_nonce_to_tx_extra(extra, extra_nonce);
if (!r) {
return couldntAddPIDNonceToTXExtra;
}
}
}
return noError;
}
bool _rct_hex_to_rct_commit(
const std::string &rct_string,
rct::key &rct_commit
) {
// rct string is empty if output is non RCT
if (rct_string.empty()) {
return false;
}
// rct_string is a string with length 64+64+64 (<rct commit> + <encrypted mask> + <rct amount>)
std::string rct_commit_str = rct_string.substr(0,64);
THROW_WALLET_EXCEPTION_IF(!string_tools::validate_hex(64, rct_commit_str), error::wallet_internal_error, "Invalid rct commit hash: " + rct_commit_str);
string_tools::hex_to_pod(rct_commit_str, rct_commit);
return true;
}
bool _rct_hex_to_decrypted_mask(
const std::string &rct_string,
const crypto::secret_key &view_secret_key,
const crypto::public_key& tx_pub_key,
uint64_t internal_output_index,
rct::key &decrypted_mask
) {
// rct string is empty if output is non RCT
if (rct_string.empty()) {
return false;
}
// rct_string is a magic value if output is RCT and coinbase
if (rct_string == "coinbase") {
decrypted_mask = rct::identity();
return true;
}
auto make_key_derivation = [&]() {
crypto::key_derivation derivation;
bool r = generate_key_derivation(tx_pub_key, view_secret_key, derivation);
THROW_WALLET_EXCEPTION_IF(!r, error::wallet_internal_error, "Failed to generate key derivation");
crypto::secret_key scalar;
crypto::derivation_to_scalar(derivation, internal_output_index, scalar);
return rct::sk2rct(scalar);
};
rct::key encrypted_mask;
// rct_string is a string with length 64+16 (<rct commit> + <amount>) if RCT version 2
if (rct_string.size() < 64 * 2) {
decrypted_mask = rct::genCommitmentMask(make_key_derivation());
return true;
}
// rct_string is a string with length 64+64+64 (<rct commit> + <encrypted mask> + <rct amount>)
std::string encrypted_mask_str = rct_string.substr(64,64);
THROW_WALLET_EXCEPTION_IF(!string_tools::validate_hex(64, encrypted_mask_str), error::wallet_internal_error, "Invalid rct mask: " + encrypted_mask_str);
string_tools::hex_to_pod(encrypted_mask_str, encrypted_mask);
//
if (encrypted_mask == rct::identity()) {
// backward compatibility; should no longer be needed after v11 mainnet fork
decrypted_mask = encrypted_mask;
return true;
}
//
// Decrypt the mask
sc_sub(decrypted_mask.bytes,
encrypted_mask.bytes,
rct::hash_to_scalar(make_key_derivation()).bytes);
return true;
}
bool _verify_sec_key(const crypto::secret_key &secret_key, const crypto::public_key &public_key)
{ // borrowed from device_default.cpp
crypto::public_key calculated_pub;
bool r = crypto::secret_key_to_public_key(secret_key, calculated_pub);
return r && public_key == calculated_pub;
}
} // unnamed namespace
//
namespace
{
template<typename T>
T pop_index(std::vector<T>& vec, size_t idx)
{
CHECK_AND_ASSERT_MES(!vec.empty(), T(), "Vector must be non-empty");
CHECK_AND_ASSERT_MES(idx < vec.size(), T(), "idx out of bounds");
T res = std::move(vec[idx]);
if (idx + 1 != vec.size()) {
vec[idx] = std::move(vec.back());
}
vec.resize(vec.size() - 1);
return res;
}
//
template<typename T>
T pop_random_value(std::vector<T>& vec)
{
CHECK_AND_ASSERT_MES(!vec.empty(), T(), "Vector must be non-empty");
size_t idx = crypto::rand<size_t>() % vec.size();
return pop_index (vec, idx);
}
}
//
//
//
// Decomposed Send procedure
void monero_transfer_utils::send_step1__prepare_params_for_get_decoys(
Send_Step1_RetVals &retVals,
//
const optional<string>& payment_id_string,
uint64_t sending_amount,
bool is_sweeping,
uint32_t simple_priority,
use_fork_rules_fn_type use_fork_rules_fn,
//
const vector<SpendableOutput> &unspent_outs,
uint64_t fee_per_b, // per v8
uint64_t fee_quantization_mask,
//
optional<uint64_t> passedIn_attemptAt_fee
) {
retVals = {};
//
if (is_sweeping) {
if (sending_amount != 0 && sending_amount != UINT64_MAX) {
THROW_WALLET_EXCEPTION_IF(
sending_amount != 0 && sending_amount != UINT64_MAX,
error::wallet_internal_error, "Ambiguous arguments; Pass sending_amount 0 while sweeping"
);
return;
}
} else { // not sweeping
if (sending_amount == 0) {
retVals.errCode = enteredAmountTooLow;
return;
}
}
//
uint32_t fake_outs_count = monero_fork_rules::fixed_mixinsize();
retVals.mixin = fake_outs_count;
//
bool use_rct = true;
bool bulletproof = true;
//
std::vector<uint8_t> extra;
CreateTransactionErrorCode tx_extra__code = _add_pid_to_tx_extra(payment_id_string, extra);
if (tx_extra__code != noError) {
retVals.errCode = tx_extra__code;
return;
}
const uint64_t base_fee = get_base_fee(fee_per_b); // in other words, fee_per_b
const uint64_t fee_multiplier = get_fee_multiplier(simple_priority, default_priority(), get_fee_algorithm(use_fork_rules_fn), use_fork_rules_fn);
//
uint64_t attempt_at_min_fee;
if (passedIn_attemptAt_fee == none) {
attempt_at_min_fee = estimate_fee(true/*use_per_byte_fee*/, true/*use_rct*/, 2/*est num inputs*/, fake_outs_count, 2, extra.size(), bulletproof, base_fee, fee_multiplier, fee_quantization_mask);
// opted to do this instead of `const uint64_t min_fee = (fee_multiplier * base_fee * estimate_tx_size(use_rct, 1, fake_outs_count, 2, extra.size(), bulletproof));`
// TODO: estimate with 1 input or 2?
} else {
attempt_at_min_fee = *passedIn_attemptAt_fee;
}
struct Total
{
static uint64_t with(uint64_t sending_amount, uint64_t fee_amount)
{
return sending_amount + fee_amount;
}
};
// fee may get changed as follows…
uint64_t potential_total; // aka balance_required
if (is_sweeping) {
potential_total = UINT64_MAX; // balance required: all
} else {
potential_total = Total::with(sending_amount, attempt_at_min_fee);
}
//
// Gather outputs and amount to use for getting decoy outputs…
uint64_t using_outs_amount = 0;
vector<SpendableOutput> remaining_unusedOuts = unspent_outs; // take copy so not to modify original
// TODO: factor this out to get spendable balance for display in the MM wallet:
while (using_outs_amount < potential_total && remaining_unusedOuts.size() > 0) {
auto out = pop_random_value(remaining_unusedOuts);
if (!use_rct && (out.rct != none && (*out.rct).empty() == false)) {
// out.rct is set by the server
continue; // skip rct outputs if not creating rct tx
}
if (out.amount < monero_fork_rules::dust_threshold()) { // amount is dusty..
if (out.rct == none || (*out.rct).empty()) {
// cout << "Found a dusty but unmixable (non-rct) output... skipping it!" << endl;
continue;
} else {
// cout << "Found a dusty but mixable (rct) amount... keeping it!" << endl;
}
}
using_outs_amount += out.amount;
// cout << "Using output: " << out.amount << " - " << out.public_key << endl;
retVals.using_outs.push_back(std::move(out));
}
retVals.spendable_balance = using_outs_amount; // must store for needMoreMoneyThanFound return
// Note: using_outs and using_outs_amount may still get modified below (so retVals.spendable_balance gets updated)
//
// if (/*using_outs.size() > 1*/ && use_rct) { // FIXME? see original core js
uint64_t needed_fee = estimate_fee(
true/*use_per_byte_fee*/, use_rct,
retVals.using_outs.size(), fake_outs_count, /*tx.dsts.size()*/1+1, extra.size(),
bulletproof, base_fee, fee_multiplier, fee_quantization_mask
);
// if newNeededFee < neededFee, use neededFee instead (should only happen on the 2nd or later times through (due to estimated fee being too low))
if (needed_fee < attempt_at_min_fee) {
needed_fee = attempt_at_min_fee;
}
//
// NOTE: needed_fee may get further modified below when !is_sweeping if using_outs_amount < total_incl_fees and gets finalized (for this function's scope) as using_fee
//
retVals.required_balance = is_sweeping ? needed_fee : potential_total; // must store for needMoreMoneyThanFound return .... NOTE: this is set to needed_fee for is_sweeping because that's literally the required balance, which an caller may want to print in case they get needMoreMoneyThanFound - note this gets updated below when !is_sweeping
//
uint64_t total_wo_fee = is_sweeping
? /*now that we know outsAmount>needed_fee*/(using_outs_amount - needed_fee)
: sending_amount;
retVals.final_total_wo_fee = total_wo_fee;
//
uint64_t total_incl_fees;
if (is_sweeping) {
if (using_outs_amount < needed_fee) { // like checking if the result of the following total_wo_fee is < 0
retVals.errCode = needMoreMoneyThanFound; // sufficiently up-to-date (for this return case) required_balance and using_outs_amount (spendable balance) will have been stored for return by this point
return;
}
total_incl_fees = using_outs_amount;
} else {
total_incl_fees = sending_amount + needed_fee; // because fee changed because using_outs.size() was updated
while (using_outs_amount < total_incl_fees && remaining_unusedOuts.size() > 0) { // add outputs 1 at a time till we either have them all or can meet the fee
{
auto out = pop_random_value(remaining_unusedOuts);
// cout << "Using output: " << out.amount << " - " << out.public_key << endl;
using_outs_amount += out.amount;
retVals.using_outs.push_back(std::move(out));
}
retVals.spendable_balance = using_outs_amount; // must store for needMoreMoneyThanFound return
//
// Recalculate fee, total incl fees
needed_fee = estimate_fee(
true/*use_per_byte_fee*/, use_rct,
retVals.using_outs.size(), fake_outs_count, /*tx.dsts.size()*/1+1, extra.size(),
bulletproof, base_fee, fee_multiplier, fee_quantization_mask
);
total_incl_fees = sending_amount + needed_fee; // because fee changed
}
retVals.required_balance = total_incl_fees; // update required_balance b/c total_incl_fees changed
}
retVals.using_fee = needed_fee;
//
// cout << "Final attempt at fee: " << needed_fee << " for " << retVals.using_outs.size() << " inputs" << endl;
// cout << "Balance to be used: " << total_incl_fees << endl;
if (using_outs_amount < total_incl_fees) {
retVals.errCode = needMoreMoneyThanFound; // sufficiently up-to-date (for this return case) required_balance and using_outs_amount (spendable balance) will have been stored for return by this point.
return;
}
//
// Change can now be calculated
uint64_t change_amount = 0; // to initialize
if (using_outs_amount > total_incl_fees) {
THROW_WALLET_EXCEPTION_IF(is_sweeping, error::wallet_internal_error, "Unexpected total_incl_fees > using_outs_amount while sweeping");
change_amount = using_outs_amount - total_incl_fees;
}
// cout << "Calculated change amount:" << change_amount << endl;
retVals.change_amount = change_amount;
//
// uint64_t tx_estimated_weight = estimate_tx_weight(true/*use_rct*/, retVals.using_outs.size(), fake_outs_count, 1+1, extra.size(), true/*bulletproof*/);
// if (tx_estimated_weight >= TX_WEIGHT_TARGET(get_upper_transaction_weight_limit(0, use_fork_rules_fn))) {
// // TODO?
// }
}
void monero_transfer_utils::send_step2__try_create_transaction(
Send_Step2_RetVals &retVals,
//
const string &from_address_string,
const string &sec_viewKey_string,
const string &sec_spendKey_string,
const string &to_address_string,
const optional<string>& payment_id_string,
uint64_t final_total_wo_fee,
uint64_t change_amount,
uint64_t fee_amount,
uint32_t simple_priority,
const vector<SpendableOutput> &using_outs,
uint64_t fee_per_b, // per v8
uint64_t fee_quantization_mask,
vector<RandomAmountOutputs> &mix_outs, // cannot be const due to convenience__create_transaction's mutability requirement
use_fork_rules_fn_type use_fork_rules_fn,
uint64_t unlock_time, // or 0
cryptonote::network_type nettype
) {
retVals = {};
//
Convenience_TransactionConstruction_RetVals create_tx__retVals;
monero_transfer_utils::convenience__create_transaction(
create_tx__retVals,
from_address_string,
sec_viewKey_string, sec_spendKey_string,
to_address_string, payment_id_string,
final_total_wo_fee, change_amount, fee_amount,
using_outs, mix_outs,
use_fork_rules_fn,
unlock_time,
nettype // TODO: move to after from_address_string
);
if (create_tx__retVals.errCode != noError) {
retVals.errCode = create_tx__retVals.errCode;
return;
}
THROW_WALLET_EXCEPTION_IF(create_tx__retVals.signed_serialized_tx_string == boost::none, error::wallet_internal_error, "Not expecting no signed_serialized_tx_string given no error");
//
size_t blob_size = *create_tx__retVals.txBlob_byteLength;
uint64_t fee_actually_needed = calculate_fee(
true/*use_per_byte_fee*/,
*create_tx__retVals.tx, blob_size,
get_base_fee(fee_per_b)/*i.e. fee_per_b*/,
get_fee_multiplier(simple_priority, default_priority(), get_fee_algorithm(use_fork_rules_fn), use_fork_rules_fn),
fee_quantization_mask
);
if (fee_actually_needed > fee_amount) {
// cout << "Need to reconstruct tx with fee of at least " << fee_actually_needed << "." << endl;
retVals.tx_must_be_reconstructed = true;
retVals.fee_actually_needed = fee_actually_needed;
return;
}
retVals.signed_serialized_tx_string = std::move(*(create_tx__retVals.signed_serialized_tx_string));
retVals.tx_hash_string = std::move(*(create_tx__retVals.tx_hash_string));
retVals.tx_key_string = std::move(*(create_tx__retVals.tx_key_string));
retVals.tx_pub_key_string = std::move(*(create_tx__retVals.tx_pub_key_string));
}
//
//
// Underlying implementations to mimic historical JS-land create_transaction / construct_tx impls
//
void monero_transfer_utils::create_transaction(
TransactionConstruction_RetVals &retVals,
const account_keys& sender_account_keys, // this will reference a particular hw::device
const uint32_t subaddr_account_idx,
const std::unordered_map<crypto::public_key, cryptonote::subaddress_index> &subaddresses,
const address_parse_info &to_addr,
uint64_t sending_amount,
uint64_t change_amount,
uint64_t fee_amount,
const vector<SpendableOutput> &outputs,
vector<RandomAmountOutputs> &mix_outs,
const std::vector<uint8_t> &extra,
use_fork_rules_fn_type use_fork_rules_fn,
uint64_t unlock_time, // or 0
bool rct,
cryptonote::network_type nettype
) {
retVals.errCode = noError;
//
// TODO: do we need to sort destinations by amount, here, according to 'decompose_destinations'?
//
uint32_t fake_outputs_count = fixed_mixinsize();
bool bulletproof = true;
rct::RangeProofType range_proof_type = bulletproof ? rct::RangeProofPaddedBulletproof : rct::RangeProofBorromean;
int bp_version = bulletproof ? (use_fork_rules_fn(HF_VERSION_SMALLER_BP, -10) ? 2 : 1) : 0;
const rct::RCTConfig rct_config {
range_proof_type,
bp_version,
};
//
if (mix_outs.size() != outputs.size() && fake_outputs_count != 0) {
retVals.errCode = wrongNumberOfMixOutsProvided;
return;
}
for (size_t i = 0; i < mix_outs.size(); i++) {
if (mix_outs[i].outputs.size() < fake_outputs_count) {
retVals.errCode = notEnoughOutputsForMixing;
return;
}
}
if (!sender_account_keys.get_device().verify_keys(sender_account_keys.m_spend_secret_key, sender_account_keys.m_account_address.m_spend_public_key)
|| !sender_account_keys.get_device().verify_keys(sender_account_keys.m_view_secret_key, sender_account_keys.m_account_address.m_view_public_key)) {
retVals.errCode = invalidSecretKeys;
return;
}
if (sending_amount > std::numeric_limits<uint64_t>::max() - change_amount
|| sending_amount + change_amount > std::numeric_limits<uint64_t>::max() - fee_amount) {
retVals.errCode = outputAmountOverflow;
return;
}
uint64_t needed_money = sending_amount + change_amount + fee_amount; // TODO: is this correct?
//
uint64_t found_money = 0;
std::vector<tx_source_entry> sources;
// TODO: log: "Selected transfers: " << outputs
for (size_t out_index = 0; out_index < outputs.size(); out_index++) {
found_money += outputs[out_index].amount;
if (found_money > UINT64_MAX) {
retVals.errCode = inputAmountOverflow;
}
auto src = tx_source_entry{};
src.amount = outputs[out_index].amount;
src.rct = outputs[out_index].rct != none && (*(outputs[out_index].rct)).empty() == false;
//
typedef cryptonote::tx_source_entry::output_entry tx_output_entry;
if (mix_outs.size() != 0) {
// Sort fake outputs by global index
std::sort(mix_outs[out_index].outputs.begin(), mix_outs[out_index].outputs.end(), [] (
RandomAmountOutput const& a,
RandomAmountOutput const& b
) {
return a.global_index < b.global_index;
});
for (
size_t j = 0;
src.outputs.size() < fake_outputs_count && j < mix_outs[out_index].outputs.size();
j++
) {
auto mix_out__output = mix_outs[out_index].outputs[j];
if (mix_out__output.global_index == outputs[out_index].global_index) {
LOG_PRINT_L2("got mixin the same as output, skipping");
continue;
}
auto oe = tx_output_entry{};
oe.first = mix_out__output.global_index;
//
crypto::public_key public_key = AUTO_VAL_INIT(public_key);
if(!string_tools::hex_to_pod(mix_out__output.public_key, public_key)) {
retVals.errCode = givenAnInvalidPubKey;
return;
}
oe.second.dest = rct::pk2rct(public_key);
//
if (mix_out__output.rct != boost::none && (*(mix_out__output.rct)).empty() == false) {
rct::key commit;
_rct_hex_to_rct_commit(*mix_out__output.rct, commit);
oe.second.mask = commit;
} else {
if (outputs[out_index].rct != boost::none && (*(outputs[out_index].rct)).empty() == false) {
retVals.errCode = mixRCTOutsMissingCommit;
return;
}
oe.second.mask = rct::zeroCommit(src.amount); //create identity-masked commitment for non-rct mix input
}
src.outputs.push_back(oe);
}
}
auto real_oe = tx_output_entry{};
real_oe.first = outputs[out_index].global_index;
//
crypto::public_key public_key = AUTO_VAL_INIT(public_key);
if(!string_tools::validate_hex(64, outputs[out_index].public_key)) {
retVals.errCode = givenAnInvalidPubKey;
return;
}
if (!string_tools::hex_to_pod(outputs[out_index].public_key, public_key)) {
retVals.errCode = givenAnInvalidPubKey;
return;
}
real_oe.second.dest = rct::pk2rct(public_key);
//
if (outputs[out_index].rct != none
&& outputs[out_index].rct->empty() == false
&& *outputs[out_index].rct != "coinbase") {
rct::key commit;
_rct_hex_to_rct_commit(*(outputs[out_index].rct), commit);
real_oe.second.mask = commit; //add commitment for real input
} else {
real_oe.second.mask = rct::zeroCommit(src.amount/*aka outputs[out_index].amount*/); //create identity-masked commitment for non-rct input
}
//
// Add real_oe to outputs
uint64_t real_output_index = src.outputs.size();
for (size_t j = 0; j < src.outputs.size(); j++) {
if (real_oe.first < src.outputs[j].first) {
real_output_index = j;
break;
}
}
src.outputs.insert(src.outputs.begin() + real_output_index, real_oe);
//
crypto::public_key tx_pub_key = AUTO_VAL_INIT(tx_pub_key);
if(!string_tools::validate_hex(64, outputs[out_index].tx_pub_key)) {
retVals.errCode = givenAnInvalidPubKey;
return;
}
string_tools::hex_to_pod(outputs[out_index].tx_pub_key, tx_pub_key);
src.real_out_tx_key = tx_pub_key;
//
src.real_out_additional_tx_keys = get_additional_tx_pub_keys_from_extra(extra);
//
src.real_output = real_output_index;
uint64_t internal_output_index = outputs[out_index].index;
src.real_output_in_tx_index = internal_output_index;
//
src.rct = outputs[out_index].rct != boost::none && (*(outputs[out_index].rct)).empty() == false;
if (src.rct) {
rct::key decrypted_mask;
bool r = _rct_hex_to_decrypted_mask(
*(outputs[out_index].rct),
sender_account_keys.m_view_secret_key,
tx_pub_key,
internal_output_index,
decrypted_mask
);
if (!r) {
retVals.errCode = cantGetDecryptedMaskFromRCTHex;
return;
}
src.mask = decrypted_mask;
// rct::key calculated_commit = rct::commit(outputs[out_index].amount, decrypted_mask);
// rct::key parsed_commit;
// _rct_hex_to_rct_commit(*(outputs[out_index].rct), parsed_commit);
// if (!(real_oe.second.mask == calculated_commit)) { // real_oe.second.mask==parsed_commit(outputs[out_index].rct)
// retVals.errCode = invalidCommitOrMaskOnOutputRCT;
// return;
// }
} else {
rct::identity(src.mask); // in the original cn_utils impl this was left as null for generate_key_image_helper_rct to fill in with identity I
}
// not doing multisig here yet
src.multisig_kLRki = rct::multisig_kLRki({rct::zero(), rct::zero(), rct::zero(), rct::zero()});
sources.push_back(src);
}
//
// TODO: if this is a multisig wallet, create a list of multisig signers we can use
std::vector<cryptonote::tx_destination_entry> splitted_dsts;
tx_destination_entry to_dst = AUTO_VAL_INIT(to_dst);
to_dst.addr = to_addr.address;
to_dst.amount = sending_amount;
to_dst.is_subaddress = to_addr.is_subaddress;
splitted_dsts.push_back(to_dst);
//
cryptonote::tx_destination_entry change_dst = AUTO_VAL_INIT(change_dst);
change_dst.amount = change_amount;
//
if (change_dst.amount == 0) {
if (splitted_dsts.size() == 1) {
// If the change is 0, send it to a random address, to avoid confusing
// the sender with a 0 amount output. We send a 0 amount in order to avoid
// letting the destination be able to work out which of the inputs is the
// real one in our rings
LOG_PRINT_L2("generating dummy address for 0 change");
cryptonote::account_base dummy;
dummy.generate();
change_dst.addr = dummy.get_keys().m_account_address;
LOG_PRINT_L2("generated dummy address for 0 change");
splitted_dsts.push_back(change_dst);
}
} else {
change_dst.addr = sender_account_keys.m_account_address;
splitted_dsts.push_back(change_dst);
}
//
// TODO: log: "sources: " << sources
if (found_money > needed_money) {
if (change_dst.amount != fee_amount) {
retVals.errCode = resultFeeNotEqualToGiven; // aka "early fee calculation != later"
return; // early
}
} else if (found_money < needed_money) {
retVals.errCode = needMoreMoneyThanFound; // TODO: return actual found_money and needed_money in generalized err params in return val
return;
}
//
cryptonote::transaction tx;
crypto::secret_key tx_key;
std::vector<crypto::secret_key> additional_tx_keys;
bool r = cryptonote::construct_tx_and_get_tx_key(
sender_account_keys, subaddresses,
sources, splitted_dsts, change_dst.addr, extra,
tx, unlock_time, tx_key, additional_tx_keys,
true, rct_config,
/*m_multisig ? &msout : */NULL
);
LOG_PRINT_L2("constructed tx, r="<<r);
if (!r) {
// TODO: return error::tx_not_constructed, sources, dsts, unlock_time, nettype
retVals.errCode = transactionNotConstructed;
return;
}
if (get_upper_transaction_weight_limit(0, use_fork_rules_fn) <= get_transaction_weight(tx)) {
// TODO: return error::tx_too_big, tx, upper_transaction_weight_limit
retVals.errCode = transactionTooBig;
return;
}
bool use_bulletproofs = !tx.rct_signatures.p.bulletproofs.empty();
THROW_WALLET_EXCEPTION_IF(use_bulletproofs != bulletproof, error::wallet_internal_error, "Expected tx use_bulletproofs to equal bulletproof flag");
//
retVals.tx = tx;
retVals.tx_key = tx_key;
retVals.additional_tx_keys = additional_tx_keys;
}
//
void monero_transfer_utils::convenience__create_transaction(
Convenience_TransactionConstruction_RetVals &retVals,
const string &from_address_string,
const string &sec_viewKey_string,
const string &sec_spendKey_string,
const string &to_address_string,
const optional<string>& payment_id_string,
uint64_t sending_amount,
uint64_t change_amount,
uint64_t fee_amount,
const vector<SpendableOutput> &outputs,
vector<RandomAmountOutputs> &mix_outs,
use_fork_rules_fn_type use_fork_rules_fn,
uint64_t unlock_time,
network_type nettype
) {
retVals.errCode = noError;
//
cryptonote::address_parse_info from_addr_info;
THROW_WALLET_EXCEPTION_IF(!cryptonote::get_account_address_from_str(from_addr_info, nettype, from_address_string), error::wallet_internal_error, "Couldn't parse from-address");
cryptonote::account_keys account_keys;
{
account_keys.m_account_address = from_addr_info.address;
//
crypto::secret_key sec_viewKey;
THROW_WALLET_EXCEPTION_IF(!string_tools::hex_to_pod(sec_viewKey_string, sec_viewKey), error::wallet_internal_error, "Couldn't parse view key");
account_keys.m_view_secret_key = sec_viewKey;
//
crypto::secret_key sec_spendKey;
THROW_WALLET_EXCEPTION_IF(!string_tools::hex_to_pod(sec_spendKey_string, sec_spendKey), error::wallet_internal_error, "Couldn't parse spend key");
account_keys.m_spend_secret_key = sec_spendKey;
}
THROW_WALLET_EXCEPTION_IF(
to_address_string.find(".") != std::string::npos, // assumed to be an OA address asXMR addresses do not have periods and OA addrs must
error::wallet_internal_error,
"Integrators must resolve OA addresses before calling Send"
); // This would be an app code fault
cryptonote::address_parse_info to_addr_info; // just in case…
if (!cryptonote::get_account_address_from_str(to_addr_info, nettype, to_address_string)) {
retVals.errCode = couldntDecodeToAddress;
return;
}
//
std::vector<uint8_t> extra;
CreateTransactionErrorCode tx_extra__code = _add_pid_to_tx_extra(payment_id_string, extra);
if (tx_extra__code != noError) {
retVals.errCode = tx_extra__code;
return;
}
bool payment_id_seen = payment_id_string != none; // logically this is true since payment_id_string has passed validation (or we'd have errored)
if (to_addr_info.is_subaddress && payment_id_seen) {
retVals.errCode = cantUsePIDWithSubAddress; // Never use a subaddress with a payment ID
return;
}
if (to_addr_info.has_payment_id) {
if (payment_id_seen) {
retVals.errCode = nonZeroPIDWithIntAddress; // can't use int addr at same time as supplying manual pid
return;
}
if (to_addr_info.is_subaddress) {
THROW_WALLET_EXCEPTION_IF(false, error::wallet_internal_error, "Unexpected is_subaddress && has_payment_id"); // should never happen
return;
}
std::string extra_nonce;
cryptonote::set_encrypted_payment_id_to_tx_extra_nonce(extra_nonce, to_addr_info.payment_id);
bool r = cryptonote::add_extra_nonce_to_tx_extra(extra, extra_nonce);
if (!r) {
retVals.errCode = couldntAddPIDNonceToTXExtra;
return;
}
payment_id_seen = true;
}
//
uint32_t subaddr_account_idx = 0;
std::unordered_map<crypto::public_key, cryptonote::subaddress_index> subaddresses;
subaddresses[account_keys.m_account_address.m_spend_public_key] = {0,0};
//
TransactionConstruction_RetVals actualCall_retVals;
create_transaction(
actualCall_retVals,
account_keys, subaddr_account_idx, subaddresses,
to_addr_info,
sending_amount, change_amount, fee_amount,
outputs, mix_outs,
extra, // TODO: move to after address
use_fork_rules_fn,
unlock_time, true/*rct*/, nettype
);
if (actualCall_retVals.errCode != noError) {
retVals.errCode = actualCall_retVals.errCode; // pass-through
return; // already set the error
}
auto txBlob = t_serializable_object_to_blob(*actualCall_retVals.tx);
size_t txBlob_byteLength = txBlob.size();
// cout << "txBlob: " << txBlob << endl;
// cout << "txBlob_byteLength: " << txBlob_byteLength << endl;
THROW_WALLET_EXCEPTION_IF(txBlob_byteLength <= 0, error::wallet_internal_error, "Expected tx blob byte length > 0");
//
// tx hash
retVals.tx_hash_string = epee::string_tools::pod_to_hex(cryptonote::get_transaction_hash(*actualCall_retVals.tx));
// signed serialized tx
retVals.signed_serialized_tx_string = epee::string_tools::buff_to_hex_nodelimer(cryptonote::tx_to_blob(*actualCall_retVals.tx));
// (concatenated) tx key
{
ostringstream oss;
oss << epee::string_tools::pod_to_hex(*actualCall_retVals.tx_key);
for (size_t i = 0; i < (*actualCall_retVals.additional_tx_keys).size(); ++i) {
oss << epee::string_tools::pod_to_hex((*actualCall_retVals.additional_tx_keys)[i]);
}
retVals.tx_key_string = oss.str();
}
{
ostringstream oss;
oss << epee::string_tools::pod_to_hex(get_tx_pub_key_from_extra(*actualCall_retVals.tx));
retVals.tx_pub_key_string = oss.str();
}
retVals.tx = *actualCall_retVals.tx; // for calculating block weight; FIXME: std::move?
//
// cout << "out 0: " << string_tools::pod_to_hex(boost::get<txout_to_key>((*(actualCall_retVals.tx)).vout[0].target).key) << endl;
// cout << "out 1: " << string_tools::pod_to_hex(boost::get<txout_to_key>((*(actualCall_retVals.tx)).vout[1].target).key) << endl;
//
retVals.txBlob_byteLength = txBlob_byteLength;
}