Files
wownero/src/cryptonote_basic/cryptonote_format_utils.cpp
T

2191 lines
90 KiB
C++

// Copyright (c) 2014-2026, 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.
//
// Parts of this file are originally copyright (c) 2012-2013 The Cryptonote developers
#include <atomic>
#include <boost/algorithm/string.hpp>
#include "wipeable_string.h"
#include "string_tools.h"
#include "string_tools_lexical.h"
#include "serialization/string.h"
#include "cryptonote_format_utils.h"
#include "cryptonote_config.h"
#include "crypto/crypto.h"
#include "crypto/hash.h"
#include "crypto/hash-ops.h"
#include "fcmp_pp/fcmp_pp_types.h"
#include "ringct/rctOps.h"
using namespace epee;
#undef MONERO_DEFAULT_LOG_CATEGORY
#define MONERO_DEFAULT_LOG_CATEGORY "cn"
// #define ENABLE_HASH_CASH_INTEGRITY_CHECK
using namespace crypto;
static std::atomic<unsigned int> default_decimal_point(CRYPTONOTE_DISPLAY_DECIMAL_POINT);
#define CHECK_AND_ASSERT_THROW_MES_L1(expr, message) {if(!(expr)) {MWARNING(message); throw std::runtime_error(message);}}
namespace cryptonote
{
static inline unsigned char *operator &(ec_point &point) {
return &reinterpret_cast<unsigned char &>(point);
}
static inline const unsigned char *operator &(const ec_point &point) {
return &reinterpret_cast<const unsigned char &>(point);
}
// a copy of rct::addKeys, since we can't link to libringct to avoid circular dependencies
static void add_public_key(crypto::public_key &AB, const crypto::public_key &A, const crypto::public_key &B) {
ge_p3 B2, A2;
CHECK_AND_ASSERT_THROW_MES_L1(ge_frombytes_vartime(&B2, &B) == 0, "ge_frombytes_vartime failed at "+boost::lexical_cast<std::string>(__LINE__));
CHECK_AND_ASSERT_THROW_MES_L1(ge_frombytes_vartime(&A2, &A) == 0, "ge_frombytes_vartime failed at "+boost::lexical_cast<std::string>(__LINE__));
ge_cached tmp2;
ge_p3_to_cached(&tmp2, &B2);
ge_p1p1 tmp3;
ge_add(&tmp3, &A2, &tmp2);
ge_p1p1_to_p3(&A2, &tmp3);
ge_p3_tobytes(&AB, &A2);
}
//---------------------------------------------------------------
static uint64_t get_transaction_weight_clawback(const bool plus, const size_t n_outputs, const size_t n_padded_outputs)
{
const uint64_t bp_base = (32 * ((plus ? 6 : 9) + 7 * 2)) / 2; // notional size of a 2 output proof, normalized to 1 proof (ie, divided by 2)
if (n_padded_outputs <= 2)
return 0;
size_t nlr = 0;
while ((1u << nlr) < n_padded_outputs)
++nlr;
nlr += 6;
const size_t bp_size = 32 * ((plus ? 6 : 9) + 2 * nlr);
CHECK_AND_ASSERT_THROW_MES_L1(n_outputs <= BULLETPROOF_MAX_OUTPUTS, "maximum number of outputs is " + std::to_string(BULLETPROOF_MAX_OUTPUTS) + " per transaction");
CHECK_AND_ASSERT_THROW_MES_L1(bp_base * n_padded_outputs >= bp_size, "Invalid bulletproof clawback: bp_base " + std::to_string(bp_base) + ", n_padded_outputs "
+ std::to_string(n_padded_outputs) + ", bp_size " + std::to_string(bp_size));
const uint64_t bp_clawback = (bp_base * n_padded_outputs - bp_size) * 4 / 5;
return bp_clawback;
}
static void get_tree_hash(const std::vector<crypto::hash>& tx_hashes, crypto::hash& h)
{
tree_hash(tx_hashes.data(), tx_hashes.size(), h);
}
static crypto::hash get_tree_hash(const std::vector<crypto::hash>& tx_hashes)
{
crypto::hash h = null_hash;
get_tree_hash(tx_hashes, h);
return h;
}
//---------------------------------------------------------------
static uint64_t get_transaction_weight_clawback(const transaction &tx, const size_t n_padded_outputs)
{
const rct::rctSig &rv = tx.rct_signatures;
const bool plus = rv.type == rct::RCTTypeBulletproofPlus || rv.type == rct::RCTTypeFcmpPlusPlus;
const size_t n_outputs = tx.vout.size();
return get_transaction_weight_clawback(plus, n_outputs, n_padded_outputs);
}
//---------------------------------------------------------------
// Helper function to group outputs by last locked block idx
static uint64_t set_tx_outs_by_last_locked_block(const cryptonote::transaction &tx,
const std::unordered_map<uint64_t, rct::key> &transparent_amount_commitments,
const uint64_t &first_unified_id,
const uint64_t block_idx,
fcmp_pp::OutsByLastLockedBlock &outs_by_last_locked_block_inout,
std::unordered_map<uint64_t/*unified_id*/, uint64_t/*last locked block_id*/> &timelocked_outputs_inout)
{
const uint64_t last_locked_block = cryptonote::get_last_locked_block_index(tx.unlock_time, block_idx);
const bool has_custom_timelock = cryptonote::is_custom_timelocked(tx.is_coinbase(),
last_locked_block,
block_idx);
for (std::size_t i = 0; i < tx.vout.size(); ++i)
{
const uint64_t unified_id = first_unified_id + i;
const auto &out = tx.vout[i];
rct::key commitment;
CHECK_AND_ASSERT_THROW_MES(cryptonote::get_commitment(tx, i, transparent_amount_commitments, commitment),
"failed to get tx commitment");
const fcmp_pp::UnifiedOutput unified_output{
.unified_id = unified_id,
.output_pair = cryptonote::to_output_pair(out.target, commitment)
};
if (has_custom_timelock)
{
timelocked_outputs_inout[unified_id] = last_locked_block;
}
outs_by_last_locked_block_inout[last_locked_block].emplace_back(unified_output);
}
return tx.vout.size();
}
//---------------------------------------------------------------
bool passes_max_size_check(const bool max_size_check, const blobdata_ref &tx_blob)
{
if (!max_size_check)
return true;
return tx_blob.size() <= get_max_tx_size();
}
//---------------------------------------------------------------
}
namespace cryptonote
{
//---------------------------------------------------------------
void get_transaction_prefix_hash(const transaction_prefix& tx, crypto::hash& h, hw::device &hwdev)
{
hwdev.get_transaction_prefix_hash(tx,h);
}
//---------------------------------------------------------------
crypto::hash get_transaction_prefix_hash(const transaction_prefix& tx, hw::device &hwdev)
{
crypto::hash h = null_hash;
get_transaction_prefix_hash(tx, h, hwdev);
return h;
}
bool expand_transaction_1(transaction &tx, bool base_only)
{
if (tx.version >= 2 && !tx.is_coinbase())
{
rct::rctSig &rv = tx.rct_signatures;
if (rv.type == rct::RCTTypeNull)
return true;
if (rv.outPk.size() != tx.vout.size())
{
LOG_PRINT_L1("Failed to parse transaction from blob, bad outPk size in tx " << get_transaction_hash(tx));
return false;
}
for (size_t n = 0; n < tx.rct_signatures.outPk.size(); ++n)
{
crypto::public_key output_public_key;
if (!get_output_public_key(tx.vout[n], output_public_key))
{
LOG_PRINT_L1("Failed to get output public key for output " << n << " in tx " << get_transaction_hash(tx));
return false;
}
rv.outPk[n].dest = rct::pk2rct(output_public_key);
}
if (!base_only)
{
const bool bulletproof = rct::is_rct_bulletproof(rv.type);
const bool bulletproof_plus = rct::is_rct_bulletproof_plus(rv.type);
if (bulletproof_plus)
{
if (rv.p.bulletproofs_plus.size() != 1)
{
LOG_PRINT_L1("Failed to parse transaction from blob, bad bulletproofs_plus size in tx " << get_transaction_hash(tx));
return false;
}
if (rv.p.bulletproofs_plus[0].L.size() < 6)
{
LOG_PRINT_L1("Failed to parse transaction from blob, bad bulletproofs_plus L size in tx " << get_transaction_hash(tx));
return false;
}
const size_t max_outputs = rct::n_bulletproof_plus_max_amounts(rv.p.bulletproofs_plus[0]);
if (max_outputs < tx.vout.size())
{
LOG_PRINT_L1("Failed to parse transaction from blob, bad bulletproofs_plus max outputs in tx " << get_transaction_hash(tx));
return false;
}
const size_t n_amounts = tx.vout.size();
CHECK_AND_ASSERT_MES(n_amounts == rv.outPk.size(), false, "Internal error filling out V");
rv.p.bulletproofs_plus[0].V.resize(n_amounts);
for (size_t i = 0; i < n_amounts; ++i)
rv.p.bulletproofs_plus[0].V[i] = rct::scalarmultKey(rv.outPk[i].mask, rct::INV_EIGHT);
}
else if (bulletproof)
{
if (rv.p.bulletproofs.size() != 1)
{
LOG_PRINT_L1("Failed to parse transaction from blob, bad bulletproofs size in tx " << get_transaction_hash(tx));
return false;
}
if (rv.p.bulletproofs[0].L.size() < 6)
{
LOG_PRINT_L1("Failed to parse transaction from blob, bad bulletproofs L size in tx " << get_transaction_hash(tx));
return false;
}
const size_t max_outputs = 1 << (rv.p.bulletproofs[0].L.size() - 6);
if (max_outputs < tx.vout.size())
{
LOG_PRINT_L1("Failed to parse transaction from blob, bad bulletproofs max outputs in tx " << get_transaction_hash(tx));
return false;
}
const size_t n_amounts = tx.vout.size();
CHECK_AND_ASSERT_MES(n_amounts == rv.outPk.size(), false, "Internal error filling out V");
rv.p.bulletproofs[0].V.resize(n_amounts);
for (size_t i = 0; i < n_amounts; ++i)
rv.p.bulletproofs[0].V[i] = rct::scalarmultKey(rv.outPk[i].mask, rct::INV_EIGHT);
}
}
}
return true;
}
//---------------------------------------------------------------
bool parse_and_validate_tx_from_blob(const blobdata_ref& tx_blob, transaction& tx, const bool max_size_check)
{
CHECK_AND_ASSERT_MES(passes_max_size_check(max_size_check, tx_blob), false, "Tx blob too big");
binary_archive<false> ba{epee::strspan<std::uint8_t>(tx_blob)};
bool r = ::serialization::serialize(ba, tx);
CHECK_AND_ASSERT_MES(r, false, "Failed to parse transaction from blob");
CHECK_AND_ASSERT_MES(expand_transaction_1(tx, false), false, "Failed to expand transaction data");
tx.invalidate_hashes();
tx.set_blob_size(tx_blob.size());
return true;
}
//---------------------------------------------------------------
bool parse_and_validate_tx_base_from_blob(const blobdata_ref& tx_blob, transaction& tx, const bool max_size_check)
{
CHECK_AND_ASSERT_MES(passes_max_size_check(max_size_check, tx_blob), false, "Tx blob too big");
binary_archive<false> ba{epee::strspan<std::uint8_t>(tx_blob)};
bool r = tx.serialize_base(ba);
CHECK_AND_ASSERT_MES(r, false, "Failed to parse transaction from blob");
CHECK_AND_ASSERT_MES(expand_transaction_1(tx, true), false, "Failed to expand transaction data");
tx.invalidate_hashes();
return true;
}
//---------------------------------------------------------------
bool parse_and_validate_tx_prefix_from_blob(const blobdata_ref& tx_blob, transaction_prefix& tx, const bool max_size_check)
{
CHECK_AND_ASSERT_MES(passes_max_size_check(max_size_check, tx_blob), false, "Tx blob too big");
binary_archive<false> ba{epee::strspan<std::uint8_t>(tx_blob)};
bool r = ::serialization::serialize_noeof(ba, tx);
CHECK_AND_ASSERT_MES(r, false, "Failed to parse transaction prefix from blob");
return true;
}
//---------------------------------------------------------------
bool parse_and_validate_tx_from_blob(const blobdata_ref& tx_blob, transaction& tx, crypto::hash& tx_hash, const bool max_size_check)
{
CHECK_AND_ASSERT_MES(passes_max_size_check(max_size_check, tx_blob), false, "Tx blob too big");
binary_archive<false> ba{epee::strspan<std::uint8_t>(tx_blob)};
bool r = ::serialization::serialize(ba, tx);
CHECK_AND_ASSERT_MES(r, false, "Failed to parse transaction from blob");
CHECK_AND_ASSERT_MES(expand_transaction_1(tx, false), false, "Failed to expand transaction data");
tx.invalidate_hashes();
tx.set_blob_size(tx_blob.size());
//TODO: validate tx
return get_transaction_hash(tx, tx_hash);
}
//---------------------------------------------------------------
bool parse_and_validate_tx_from_blob(const blobdata_ref& tx_blob, transaction& tx, crypto::hash& tx_hash, crypto::hash& tx_prefix_hash, const bool max_size_check)
{
if (!parse_and_validate_tx_from_blob(tx_blob, tx, tx_hash, max_size_check))
return false;
get_transaction_prefix_hash(tx, tx_prefix_hash);
return true;
}
//------------------------------------------------------------------
size_t get_tx_version(const blobdata_ref tx_blob)
{
size_t version;
const char* begin = reinterpret_cast<const char*>(tx_blob.data());
const char* end = begin + tx_blob.size();
int read = tools::read_varint(begin, end, version);
if (read <= 0)
throw std::runtime_error("Internal error getting transaction version");
return version;
}
//---------------------------------------------------------------
bool is_v1_tx(const blobdata_ref& tx_blob)
{
uint64_t version;
const char* begin = static_cast<const char*>(tx_blob.data());
const char* end = begin + tx_blob.size();
int read = tools::read_varint(begin, end, version);
if (read <= 0)
throw std::runtime_error("Internal error getting transaction version");
return version <= 1;
}
//---------------------------------------------------------------
bool is_v1_tx(const blobdata& tx_blob)
{
return is_v1_tx(blobdata_ref{tx_blob.data(), tx_blob.size()});
}
//---------------------------------------------------------------
bool get_transaction_unprunable_summary(const blobdata_ref tx_blob, unprunable_summary_t &summary_out)
{
const unsigned char *p = reinterpret_cast<const unsigned char*>(tx_blob.data());
const unsigned char *end = reinterpret_cast<const unsigned char*>(tx_blob.data() + tx_blob.size());
#define READ_VARINT(v) if (tools::read_varint(p, end, v) <= 0) return false
#define READ_BYTE(v) if (end <= p ) { return false; } else { v = *p; ++p; }
#define SKIP(n) if (end - p < static_cast<std::ptrdiff_t>(n)) { return false; } else { p += (n); }
// read and validate tx version
READ_VARINT(summary_out.version);
if (summary_out.version < 1 || summary_out.version > 2)
return false;
// skip unlock_time
std::uint64_t dummy;
READ_VARINT(dummy);
// read number of inputs
READ_VARINT(summary_out.n_inputs);
// skip n_inputs inputs, checking for coinbase inputs
summary_out.is_coinbase = false;
for (std::size_t i = 0; i < summary_out.n_inputs; ++i)
{
std::size_t input_tag = 0;
READ_BYTE(input_tag);
switch (input_tag)
{
case 0xff: //txin_gen
summary_out.is_coinbase = true;
READ_VARINT(dummy); //height
break;
case 0x02: //txin_to_key
READ_VARINT(dummy); //amount
READ_VARINT(input_tag); //key_offsets.size()
for (;input_tag-->0;)
READ_VARINT(dummy);
SKIP(32); //k_image
break;
default:
return false;
}
}
// read number of outputs
READ_VARINT(summary_out.n_outputs);
// skip n_outputs outputs
for (std::size_t i = 0; i < summary_out.n_outputs; ++i)
{
READ_VARINT(dummy); //amount
std::size_t output_tag = 0;
READ_BYTE(output_tag); //target variant tag
std::ptrdiff_t output_length = 0;
switch (output_tag)
{
case 0x01: //txout_to_carrot_v1
output_length = 32 + 3 + 16;
static_assert(sizeof(cryptonote::txout_to_carrot_v1) == 32+3+16, "Unexpected sizeof txout_to_carrot_v1");
break;
case 0x02: //txout_to_key
output_length = 32;
static_assert(sizeof(cryptonote::txout_to_key) == 32, "Unexpected sizeof txout_to_key");
break;
case 0x03: //txout_to_tagged_key
output_length = 32 + 1;
static_assert(sizeof(cryptonote::txout_to_tagged_key) == 32+1, "Unexpected sizeof txout_to_tagged_key");
break;
default:
return false;
}
SKIP(output_length);
}
// read extra length and skip that
READ_VARINT(summary_out.extra_len);
if (summary_out.extra_len > CRYPTONOTE_MAX_TX_SIZE)
return false;
SKIP(summary_out.extra_len);
// now `p` is at end of tx prefix...
summary_out.prefix_size = reinterpret_cast<const char*>(p) - tx_blob.data();
if (2 == summary_out.version)
{
// read RingCT type
std::uint8_t rct_type = std::numeric_limits<std::uint8_t>::max();
READ_BYTE(rct_type);
if (rct_type > rct::RCTTypeFcmpPlusPlus)
return false;
// skip unprunable RingCT fields
if (rct_type != rct::RCTTypeNull)
{
// skip txnFee
READ_VARINT(dummy);
// if RCTTypeSimple, skip pseudoOutputs
if (rct_type == rct::RCTTypeSimple)
{
SKIP(32 * summary_out.n_inputs);
}
// skip ECDH info and amount commitments
const bool short_amount = rct_type >= rct::RCTTypeBulletproof2;
const std::ptrdiff_t ecdh_tuple_len = short_amount ? 8 : 64;
const std::ptrdiff_t output_stuff_len = summary_out.n_outputs * (ecdh_tuple_len + 32);
SKIP(output_stuff_len);
}
}
#undef READ_VARINT
#undef READ_BYTE
#undef SKIP
// now `p` is at end of unprunable part of v2 tx...
summary_out.unprunable_size = reinterpret_cast<const char*>(p) - tx_blob.data();
return true;
}
//---------------------------------------------------------------
bool generate_key_image_helper(const account_keys& ack, const std::unordered_map<crypto::public_key, subaddress_index>& subaddresses, const crypto::public_key& out_key, const crypto::public_key& tx_public_key, const std::vector<crypto::public_key>& additional_tx_public_keys, size_t real_output_index, keypair& in_ephemeral, crypto::key_image& ki, hw::device &hwdev)
{
crypto::key_derivation recv_derivation{};
bool r = hwdev.generate_key_derivation(tx_public_key, ack.m_view_secret_key, recv_derivation);
if (!r)
{
MWARNING("key image helper: failed to generate_key_derivation(" << tx_public_key << ", <viewkey>)");
memcpy(&recv_derivation, rct::identity().bytes, sizeof(recv_derivation));
}
std::vector<crypto::key_derivation> additional_recv_derivations;
for (size_t i = 0; i < additional_tx_public_keys.size(); ++i)
{
crypto::key_derivation additional_recv_derivation{};
r = hwdev.generate_key_derivation(additional_tx_public_keys[i], ack.m_view_secret_key, additional_recv_derivation);
if (!r)
{
MWARNING("key image helper: failed to generate_key_derivation(" << additional_tx_public_keys[i] << ", <viewkey>)");
memcpy(&additional_recv_derivation, rct::identity().bytes, sizeof(additional_recv_derivation));
}
additional_recv_derivations.push_back(additional_recv_derivation);
}
boost::optional<subaddress_receive_info> subaddr_recv_info = is_out_to_acc_precomp(subaddresses, out_key, recv_derivation, additional_recv_derivations, real_output_index,hwdev);
CHECK_AND_ASSERT_MES(subaddr_recv_info, false, "key image helper: given output pubkey doesn't seem to belong to this address");
return generate_key_image_helper_precomp(ack, out_key, subaddr_recv_info->derivation, real_output_index, subaddr_recv_info->index, in_ephemeral, ki, hwdev);
}
//---------------------------------------------------------------
bool generate_key_image_helper_precomp(const account_keys& ack, const crypto::public_key& out_key, const crypto::key_derivation& recv_derivation, size_t real_output_index, const subaddress_index& received_index, keypair& in_ephemeral, crypto::key_image& ki, hw::device &hwdev)
{
if (hwdev.compute_key_image(ack, out_key, recv_derivation, real_output_index, received_index, in_ephemeral, ki))
{
return true;
}
if (ack.m_spend_secret_key == crypto::null_skey)
{
// for watch-only wallet, simply copy the known output pubkey
in_ephemeral.pub = out_key;
in_ephemeral.sec = crypto::null_skey;
}
else
{
// derive secret key with subaddress - step 1: original CN derivation
crypto::secret_key scalar_step1;
crypto::secret_key spend_skey = crypto::null_skey;
if (ack.m_multisig_keys.empty())
{
// if not multisig, use normal spend skey
spend_skey = ack.m_spend_secret_key;
}
else
{
// if multisig, use sum of multisig privkeys (local account's share of aggregate spend key)
for (const auto &multisig_key : ack.m_multisig_keys)
{
sc_add((unsigned char*)spend_skey.data,
(const unsigned char*)multisig_key.data,
(const unsigned char*)spend_skey.data);
}
}
// computes Hs(a*R || idx) + b
hwdev.derive_secret_key(recv_derivation, real_output_index, spend_skey, scalar_step1);
// step 2: add Hs(a || index_major || index_minor)
crypto::secret_key subaddr_sk;
crypto::secret_key scalar_step2;
if (received_index.is_zero())
{
scalar_step2 = scalar_step1; // treat index=(0,0) as a special case representing the main address
}
else
{
subaddr_sk = hwdev.get_subaddress_secret_key(ack.m_view_secret_key, received_index);
hwdev.sc_secret_add(scalar_step2, scalar_step1,subaddr_sk);
}
in_ephemeral.sec = scalar_step2;
if (ack.m_multisig_keys.empty())
{
// when not in multisig, we know the full spend secret key, so the output pubkey can be obtained by scalarmultBase
CHECK_AND_ASSERT_MES(hwdev.secret_key_to_public_key(in_ephemeral.sec, in_ephemeral.pub), false, "Failed to derive public key");
}
else
{
// when in multisig, we only know the partial spend secret key. but we do know the full spend public key, so the output pubkey can be obtained by using the standard CN key derivation
CHECK_AND_ASSERT_MES(hwdev.derive_public_key(recv_derivation, real_output_index, ack.m_account_address.m_spend_public_key, in_ephemeral.pub), false, "Failed to derive public key");
// and don't forget to add the contribution from the subaddress part
if (!received_index.is_zero())
{
crypto::public_key subaddr_pk;
CHECK_AND_ASSERT_MES(hwdev.secret_key_to_public_key(subaddr_sk, subaddr_pk), false, "Failed to derive public key");
add_public_key(in_ephemeral.pub, in_ephemeral.pub, subaddr_pk);
}
}
CHECK_AND_ASSERT_MES(in_ephemeral.pub == out_key,
false, "key image helper precomp: given output pubkey doesn't match the derived one");
}
hwdev.generate_key_image(in_ephemeral.pub, in_ephemeral.sec, ki);
return true;
}
//---------------------------------------------------------------
bool parse_amount(uint64_t& amount, const std::string& str_amount_)
{
std::string str_amount = str_amount_;
boost::algorithm::trim(str_amount);
size_t point_index = str_amount.find_first_of('.');
size_t fraction_size;
if (std::string::npos != point_index)
{
fraction_size = str_amount.size() - point_index - 1;
while (default_decimal_point < fraction_size && '0' == str_amount.back())
{
str_amount.erase(str_amount.size() - 1, 1);
--fraction_size;
}
if (default_decimal_point < fraction_size)
return false;
str_amount.erase(point_index, 1);
}
else
{
fraction_size = 0;
}
if (str_amount.empty())
return false;
if (fraction_size < default_decimal_point)
{
str_amount.append(default_decimal_point - fraction_size, '0');
}
return string_tools::get_xtype_from_string(amount, str_amount);
}
//---------------------------------------------------------------
uint64_t get_fcmp_pp_prefix_weight_v1(const size_t n_inputs, const size_t n_outputs, const size_t extra_len)
{
MTRACE(__func__ << "(n_inputs=" << n_inputs << ", n_outputs=" << n_outputs << ", extra_len=" << extra_len);
CHECK_AND_ASSERT_MES(n_inputs && n_inputs <= FCMP_PLUS_PLUS_MAX_INPUTS,
std::numeric_limits<uint64_t>::max(),
"invalid n_inputs: " << n_inputs);
CHECK_AND_ASSERT_MES(n_outputs >= 2 && n_outputs <= FCMP_PLUS_PLUS_MAX_OUTPUTS,
std::numeric_limits<uint64_t>::max(),
"invalid n_outputs: " << n_outputs);
CHECK_AND_ASSERT_MES(extra_len <= MAX_TX_EXTRA_SIZE,
std::numeric_limits<uint64_t>::max(),
"invalid extra_len: " << extra_len);
static constexpr uint64_t txin_to_key_weight = 1 /*amount=0*/ + 1 /*key_offsets.size()=0*/ + 32 /*k_image*/;
static constexpr uint64_t txout_to_carrot_weight = 32 /*key*/ + 3 /*view_tag*/ + 16 /*encrypted_janus_anchor*/;
static constexpr uint64_t tx_out_weight = 1 /*amount=0*/ + txout_to_carrot_weight + 1 /*txout_target_v tag*/;
// varint len bumps from 2 to 3 at 16384
static_assert(16384 > FCMP_PLUS_PLUS_MAX_INPUTS, "16384 expected > FCMP_PLUS_PLUS_MAX_INPUTS");
static_assert(16384 > MAX_TX_EXTRA_SIZE, "16384 expected > MAX_TX_EXTRA_SIZE");
// varint len bumps from 1 to 2 at 128
static_assert(128 > FCMP_PLUS_PLUS_MAX_OUTPUTS, "128 expected > FCMP_PLUS_PLUS_MAX_OUTPUTS");
return
1 /*version=2*/
+ 1 /*unlock_time=0*/
+ (n_inputs >= 128 ? 2 : 1) /*vin.size()<=FCMP_PLUS_PLUS_MAX_INPUTS*/
+ n_inputs * (txin_to_key_weight /*txin_to_key*/ + 1 /*txin_v tag*/)
+ 1 /*vout.size()<=FCMP_PLUS_PLUS_MAX_OUTPUTS*/
+ (n_outputs * tx_out_weight /*tx_out*/)
+ (extra_len >= 128 ? 2 : 1) /*extra.size()*/
+ extra_len;
}
//---------------------------------------------------------------
uint64_t get_fcmp_pp_unprunable_weight_v1(const size_t n_inputs, const size_t n_outputs, const size_t extra_len)
{
MTRACE(__func__ << "(n_inputs=" << n_inputs << ", n_outputs=" << n_outputs << ", extra_len=" << extra_len);
const uint64_t prefix_weight = get_fcmp_pp_prefix_weight_v1(n_inputs, n_outputs, extra_len);
if (prefix_weight == std::numeric_limits<uint64_t>::max())
return prefix_weight;
static constexpr uint64_t max_u64_varint_len = 10; // size of varint storing 2**64-1
static constexpr uint64_t rct_sig_base_per_out_weight = 8 /*ecdhInfo.at(i).amount*/ + 32 /*outPk.at(i).mask*/;
return prefix_weight
+ 1 /*type*/
+ max_u64_varint_len /*txnFee*/
+ (n_outputs * rct_sig_base_per_out_weight);
}
//---------------------------------------------------------------
uint64_t get_fcmp_pp_transaction_weight_v1(const size_t n_inputs, const size_t n_outputs, const size_t extra_len)
{
MTRACE(__func__ << "(n_inputs=" << n_inputs << ", n_outputs=" << n_outputs << ", extra_len=" << extra_len);
const uint64_t unprunable_weight = get_fcmp_pp_unprunable_weight_v1(n_inputs, n_outputs, extra_len);
if (unprunable_weight == std::numeric_limits<uint64_t>::max())
return unprunable_weight;
static constexpr uint64_t max_block_index_varint_len = 5; // size of varint storing CRYPTONOTE_MAX_BLOCK_NUMBER
static constexpr uint64_t rerandomized_output_weight = FCMP_PP_INPUT_TUPLE_SIZE_V1 + 32 /*C~ AKA pseudoOut*/;
const uint64_t total_sal_weight = n_inputs * (rerandomized_output_weight + FCMP_PP_SAL_PROOF_SIZE_V1);
const uint64_t misc_fcmp_pp_weight = max_block_index_varint_len /*reference_block*/ + 1 /*n_tree_layers*/;
// Calculate deterministic bulletproofs size (assumes canonical BP format)
size_t nrl = 0, n_padded_outputs;
while ((n_padded_outputs = (1u << nrl)) < n_outputs)
++nrl;
nrl += 6;
uint64_t bp_weight = 32 * (6 + 2 * nrl) + 2;
bp_weight += 1 /*nbp*/;
// There's a few reasons why we treat n_tree_layers as a fixed value for weight calculation:
// a. If we took n_tree_layers into account when calculating weight, then fee calculation
// would be a function of the number of layers in the FCMP tree. This has a couple
// implications:
// i. To determine the "correct" fee in multi-signer/cold-signer contexts, signers
// would have to transmit and agree upon what the current n_tree_layers value is,
// which complicates these protocols, and is inherently difficult to validate
// for offline signers. It also just complicates the process for normal wallets.
// ii. If signers need guarantees that a signature for a transaction proposal with a
// certain fee isn't reused for similar transaction but with a different
// n_tree_layers, and thus weight, then n_tree_layers would have to be included
// in rctSigBase and hashed into the signable_tx_hash, which means an extra byte
// per pruned transaction when wallets are refreshing. Also, more subjectively,
// putting n_tree_layers into rctSigBase feels misplaced.
// b. Dropping the weight for low values of n_tree_layers directly incentivizes spenders of
// old enotes to use as small a value of n_tree_layers as possible, which hurts their
// anonymity.
//
// We chose 7 specifically because at the time of writing (9 April 2025), the current layer size
// of the Monero mainnet would be 6. 7 is approaching relatively quickly, and would be the value
// for many decades at the current tx volume.
static constexpr size_t fake_n_tree_layers = 7;
const uint64_t fcmp_weight = fcmp_pp::membership_proof_len(n_inputs, fake_n_tree_layers);
const uint64_t rct_sig_prunable_weight = bp_weight + total_sal_weight + misc_fcmp_pp_weight + fcmp_weight;
return unprunable_weight + rct_sig_prunable_weight;
}
//---------------------------------------------------------------
uint64_t get_fcmp_pp_transaction_weight_v1(const transaction_prefix &tx_prefix)
{
return get_fcmp_pp_transaction_weight_v1(tx_prefix.vin.size(), tx_prefix.vout.size(), tx_prefix.extra.size());
}
//---------------------------------------------------------------
uint64_t get_transaction_weight(const transaction &tx, size_t blob_size)
{
CHECK_AND_ASSERT_MES(!tx.pruned, std::numeric_limits<uint64_t>::max(), "get_transaction_weight does not support pruned txes");
CHECK_AND_ASSERT_MES(tx.rct_signatures.type <= rct::RCTTypeFcmpPlusPlus,
std::numeric_limits<uint64_t>::max(),
"get_transaction_weight does not support transactions newer than FCMP++ v1");
if (tx.version < 2)
return blob_size;
else if (tx.rct_signatures.type == rct::RCTTypeFcmpPlusPlus)
return get_fcmp_pp_transaction_weight_v1(tx);
const rct::rctSig &rv = tx.rct_signatures;
const bool bulletproof = rct::is_rct_bulletproof(rv.type);
const bool bulletproof_plus = rct::is_rct_bulletproof_plus(rv.type);
if (!bulletproof && !bulletproof_plus)
return blob_size;
const size_t n_padded_outputs = bulletproof_plus ? rct::n_bulletproof_plus_max_amounts(rv.p.bulletproofs_plus) : rct::n_bulletproof_max_amounts(rv.p.bulletproofs);
uint64_t bp_clawback = get_transaction_weight_clawback(tx, n_padded_outputs);
CHECK_AND_ASSERT_THROW_MES_L1(bp_clawback <= std::numeric_limits<uint64_t>::max() - blob_size, "Weight overflow");
return blob_size + bp_clawback;
}
//---------------------------------------------------------------
uint64_t get_pruned_transaction_weight(const transaction &tx)
{
CHECK_AND_ASSERT_MES(tx.pruned, std::numeric_limits<uint64_t>::max(), "get_pruned_transaction_weight does not support non pruned txes");
CHECK_AND_ASSERT_MES(tx.version >= 2, std::numeric_limits<uint64_t>::max(), "get_pruned_transaction_weight does not support v1 txes");
if (tx.rct_signatures.type == rct::RCTTypeFcmpPlusPlus)
return get_fcmp_pp_transaction_weight_v1(tx);
CHECK_AND_ASSERT_MES(tx.rct_signatures.type == rct::RCTTypeBulletproof2 || tx.rct_signatures.type == rct::RCTTypeCLSAG || tx.rct_signatures.type == rct::RCTTypeBulletproofPlus,
std::numeric_limits<uint64_t>::max(), "Unsupported rct_signatures type in get_pruned_transaction_weight");
CHECK_AND_ASSERT_MES(!tx.vin.empty(), std::numeric_limits<uint64_t>::max(), "empty vin");
CHECK_AND_ASSERT_MES(tx.vin[0].type() == typeid(cryptonote::txin_to_key), std::numeric_limits<uint64_t>::max(), "empty vin");
// get pruned data size
std::ostringstream s;
binary_archive<true> a(s);
::serialization::serialize(a, const_cast<transaction&>(tx));
uint64_t weight = s.str().size(), extra;
// nbps (technically varint)
weight += 1;
// calculate deterministic bulletproofs size (assumes canonical BP format)
size_t nrl = 0, n_padded_outputs;
while ((n_padded_outputs = (1u << nrl)) < tx.vout.size())
++nrl;
nrl += 6;
extra = 32 * ((rct::is_rct_bulletproof_plus(tx.rct_signatures.type) ? 6 : 9) + 2 * nrl) + 2;
weight += extra;
// calculate deterministic CLSAG/MLSAG data size
// TODO: update for fcmp_pp
const size_t ring_size = boost::get<cryptonote::txin_to_key>(tx.vin[0]).key_offsets.size();
if (rct::is_rct_clsag(tx.rct_signatures.type))
extra = tx.vin.size() * (ring_size + 2) * 32;
else
extra = tx.vin.size() * (ring_size * (1 + 1) * 32 + 32 /* cc */);
weight += extra;
// calculate deterministic pseudoOuts size
extra = 32 * (tx.vin.size());
weight += extra;
// clawback
uint64_t bp_clawback = get_transaction_weight_clawback(tx, n_padded_outputs);
CHECK_AND_ASSERT_THROW_MES_L1(bp_clawback <= std::numeric_limits<uint64_t>::max() - weight, "Weight overflow");
weight += bp_clawback;
return weight;
}
//---------------------------------------------------------------
uint64_t get_transaction_weight(const transaction &tx)
{
size_t blob_size;
if (tx.is_blob_size_valid())
{
blob_size = tx.blob_size;
}
else
{
std::ostringstream s;
binary_archive<true> a(s);
::serialization::serialize(a, const_cast<transaction&>(tx));
blob_size = s.str().size();
}
return get_transaction_weight(tx, blob_size);
}
//---------------------------------------------------------------
uint64_t get_transaction_blob_size(const transaction& tx)
{
if (!tx.is_blob_size_valid())
{
const cryptonote::blobdata tx_blob = tx_to_blob(tx);
tx.set_blob_size(tx_blob.size());
}
CHECK_AND_ASSERT_THROW_MES(tx.is_blob_size_valid(), "BUG: blob size valid not set");
return tx.blob_size;
}
//---------------------------------------------------------------
bool prune_transaction_blob(cryptonote::blobdata &tx_blob)
{
// Deserialize unprunable part to retrieve size
unprunable_summary_t desc;
if (!get_transaction_unprunable_summary(tx_blob, desc))
return false;
CHECK_AND_ASSERT_MES(desc.unprunable_size <= tx_blob.size(), false, "Unprunable size is larger than tx blob");
tx_blob.resize(desc.unprunable_size);
return true;
}
//---------------------------------------------------------------
bool get_tx_fee(const transaction& tx, uint64_t & fee)
{
if (tx.version > 1)
{
fee = tx.rct_signatures.txnFee;
return true;
}
uint64_t amount_in = 0;
uint64_t amount_out = 0;
for(auto& in: tx.vin)
{
CHECK_AND_ASSERT_MES(in.type() == typeid(txin_to_key), 0, "unexpected type id in transaction");
amount_in += boost::get<txin_to_key>(in).amount;
}
for(auto& o: tx.vout)
amount_out += o.amount;
CHECK_AND_ASSERT_MES(amount_in >= amount_out, false, "transaction spend (" <<amount_in << ") more than it has (" << amount_out << ")");
fee = amount_in - amount_out;
return true;
}
//---------------------------------------------------------------
uint64_t get_tx_fee(const transaction& tx)
{
uint64_t r = 0;
if(!get_tx_fee(tx, r))
return 0;
return r;
}
//---------------------------------------------------------------
bool parse_tx_extra(const std::vector<uint8_t>& tx_extra, std::vector<tx_extra_field>& tx_extra_fields)
{
tx_extra_fields.clear();
if(tx_extra.empty())
return true;
binary_archive<false> ar{epee::to_span(tx_extra)};
do
{
tx_extra_field field;
bool r = ::do_serialize(ar, field);
CHECK_AND_NO_ASSERT_MES_L1(r, false, "failed to deserialize extra field. extra = " << string_tools::buff_to_hex_nodelimer(std::string(reinterpret_cast<const char*>(tx_extra.data()), tx_extra.size())));
tx_extra_fields.push_back(field);
} while (!ar.eof());
CHECK_AND_NO_ASSERT_MES_L1(::serialization::check_stream_state(ar), false, "failed to deserialize extra field. extra = " << string_tools::buff_to_hex_nodelimer(std::string(reinterpret_cast<const char*>(tx_extra.data()), tx_extra.size())));
return true;
}
//---------------------------------------------------------------
template<typename T>
static bool pick(binary_archive<true> &ar, std::vector<tx_extra_field> &fields, uint8_t tag)
{
std::vector<tx_extra_field>::iterator it;
while ((it = std::find_if(fields.begin(), fields.end(), [](const tx_extra_field &f) { return f.type() == typeid(T); })) != fields.end())
{
bool r = ::do_serialize(ar, tag);
CHECK_AND_NO_ASSERT_MES_L1(r, false, "failed to serialize tx extra field");
r = ::do_serialize(ar, boost::get<T>(*it));
CHECK_AND_NO_ASSERT_MES_L1(r, false, "failed to serialize tx extra field");
fields.erase(it);
}
return true;
}
//---------------------------------------------------------------
bool sort_tx_extra(const std::vector<uint8_t>& tx_extra, std::vector<uint8_t> &sorted_tx_extra, bool allow_partial)
{
std::vector<tx_extra_field> tx_extra_fields;
if(tx_extra.empty())
{
sorted_tx_extra.clear();
return true;
}
binary_archive<false> ar{epee::to_span(tx_extra)};
size_t processed = 0;
do
{
tx_extra_field field;
bool r = ::do_serialize(ar, field);
if (!r)
{
MWARNING("failed to deserialize extra field. extra = " << string_tools::buff_to_hex_nodelimer(std::string(reinterpret_cast<const char*>(tx_extra.data()), tx_extra.size())));
if (!allow_partial)
return false;
break;
}
tx_extra_fields.push_back(field);
processed = ar.getpos();
} while (!ar.eof());
if (!::serialization::check_stream_state(ar))
{
MWARNING("failed to deserialize extra field. extra = " << string_tools::buff_to_hex_nodelimer(std::string(reinterpret_cast<const char*>(tx_extra.data()), tx_extra.size())));
if (!allow_partial)
return false;
}
MTRACE("Sorted " << processed << "/" << tx_extra.size());
std::ostringstream oss;
binary_archive<true> nar(oss);
// sort by:
if (!pick<tx_extra_pub_key>(nar, tx_extra_fields, TX_EXTRA_TAG_PUBKEY)) return false;
if (!pick<tx_extra_additional_pub_keys>(nar, tx_extra_fields, TX_EXTRA_TAG_ADDITIONAL_PUBKEYS)) return false;
if (!pick<tx_extra_nonce>(nar, tx_extra_fields, TX_EXTRA_NONCE)) return false;
if (!pick<tx_extra_merge_mining_tag>(nar, tx_extra_fields, TX_EXTRA_MERGE_MINING_TAG)) return false;
if (!pick<tx_extra_mysterious_minergate>(nar, tx_extra_fields, TX_EXTRA_MYSTERIOUS_MINERGATE_TAG)) return false;
if (!pick<tx_extra_padding>(nar, tx_extra_fields, TX_EXTRA_TAG_PADDING)) return false;
// if not empty, someone added a new type and did not add a case above
if (!tx_extra_fields.empty())
{
MERROR("tx_extra_fields not empty after sorting, someone forgot to add a case above");
return false;
}
std::string oss_str = oss.str();
if (allow_partial && processed < tx_extra.size())
{
MDEBUG("Appending unparsed data");
oss_str += std::string((const char*)tx_extra.data() + processed, tx_extra.size() - processed);
}
sorted_tx_extra = std::vector<uint8_t>(oss_str.begin(), oss_str.end());
return true;
}
//---------------------------------------------------------------
crypto::public_key get_tx_pub_key_from_extra(const std::vector<uint8_t>& tx_extra, size_t pk_index)
{
std::vector<tx_extra_field> tx_extra_fields;
parse_tx_extra(tx_extra, tx_extra_fields);
tx_extra_pub_key pub_key_field;
if(!find_tx_extra_field_by_type(tx_extra_fields, pub_key_field, pk_index))
return null_pkey;
return pub_key_field.pub_key;
}
//---------------------------------------------------------------
crypto::public_key get_tx_pub_key_from_extra(const transaction_prefix& tx_prefix, size_t pk_index)
{
return get_tx_pub_key_from_extra(tx_prefix.extra, pk_index);
}
//---------------------------------------------------------------
crypto::public_key get_tx_pub_key_from_extra(const transaction& tx, size_t pk_index)
{
return get_tx_pub_key_from_extra(tx.extra, pk_index);
}
//---------------------------------------------------------------
bool add_tx_pub_key_to_extra(transaction& tx, const crypto::public_key& tx_pub_key)
{
return add_tx_pub_key_to_extra(tx.extra, tx_pub_key);
}
//---------------------------------------------------------------
bool add_tx_pub_key_to_extra(transaction_prefix& tx, const crypto::public_key& tx_pub_key)
{
return add_tx_pub_key_to_extra(tx.extra, tx_pub_key);
}
//---------------------------------------------------------------
bool add_tx_pub_key_to_extra(std::vector<uint8_t>& tx_extra, const crypto::public_key& tx_pub_key)
{
tx_extra.resize(tx_extra.size() + 1 + sizeof(crypto::public_key));
tx_extra[tx_extra.size() - 1 - sizeof(crypto::public_key)] = TX_EXTRA_TAG_PUBKEY;
*reinterpret_cast<crypto::public_key*>(&tx_extra[tx_extra.size() - sizeof(crypto::public_key)]) = tx_pub_key;
return true;
}
//---------------------------------------------------------------
std::vector<crypto::public_key> get_additional_tx_pub_keys_from_extra(const std::vector<uint8_t>& tx_extra)
{
// parse
std::vector<tx_extra_field> tx_extra_fields;
parse_tx_extra(tx_extra, tx_extra_fields);
// find corresponding field
tx_extra_additional_pub_keys additional_pub_keys;
if(!find_tx_extra_field_by_type(tx_extra_fields, additional_pub_keys))
return {};
return additional_pub_keys.data;
}
//---------------------------------------------------------------
std::vector<crypto::public_key> get_additional_tx_pub_keys_from_extra(const transaction_prefix& tx)
{
return get_additional_tx_pub_keys_from_extra(tx.extra);
}
//---------------------------------------------------------------
bool add_additional_tx_pub_keys_to_extra(std::vector<uint8_t>& tx_extra, const std::vector<crypto::public_key>& additional_pub_keys)
{
// convert to variant
tx_extra_field field = tx_extra_additional_pub_keys{ additional_pub_keys };
// serialize
std::ostringstream oss;
binary_archive<true> ar(oss);
bool r = ::do_serialize(ar, field);
CHECK_AND_NO_ASSERT_MES_L1(r, false, "failed to serialize tx extra additional tx pub keys");
// append
std::string tx_extra_str = oss.str();
size_t pos = tx_extra.size();
tx_extra.resize(tx_extra.size() + tx_extra_str.size());
memcpy(&tx_extra[pos], tx_extra_str.data(), tx_extra_str.size());
return true;
}
//---------------------------------------------------------------
bool add_extra_nonce_to_tx_extra(std::vector<uint8_t>& tx_extra, const blobdata& extra_nonce)
{
CHECK_AND_ASSERT_MES(extra_nonce.size() <= TX_EXTRA_NONCE_MAX_COUNT, false, "extra nonce could be 255 bytes max");
size_t start_pos = tx_extra.size();
const std::size_t len_varint_bytes = tools::get_varint_byte_size(extra_nonce.size());
tx_extra.resize(tx_extra.size() + 1 + len_varint_bytes + extra_nonce.size());
//write tag
tx_extra[start_pos] = TX_EXTRA_NONCE;
//write len
++start_pos;
unsigned char * vp = tx_extra.data() + start_pos;
tools::write_varint(vp, extra_nonce.size());
assert(vp == tx_extra.data() + tx_extra.size() - extra_nonce.size());
//write data
start_pos += len_varint_bytes;
if (!extra_nonce.empty())
memcpy(&tx_extra[start_pos], extra_nonce.data(), extra_nonce.size());
return true;
}
//---------------------------------------------------------------
bool add_mm_merkle_root_to_tx_extra(std::vector<uint8_t>& tx_extra, const crypto::hash& mm_merkle_root, uint64_t mm_merkle_tree_depth)
{
size_t start_pos = tx_extra.size();
static const size_t max_varint_size = 16;
tx_extra.resize(tx_extra.size() + 2 + 32 + max_varint_size);
//write tag
tx_extra[start_pos] = TX_EXTRA_MERGE_MINING_TAG;
//write data size
++start_pos;
const off_t len_bytes = start_pos;
// one byte placeholder for length since we'll only know the size later after writing a varint
tx_extra[start_pos] = 0;
//write depth varint
++start_pos;
uint8_t *ptr = &tx_extra[start_pos], *start = ptr;
tools::write_varint(ptr, mm_merkle_tree_depth);
//write data
const size_t varint_size = ptr - start;
start_pos += varint_size;
memcpy(&tx_extra[start_pos], &mm_merkle_root, 32);
tx_extra.resize(tx_extra.size() - (max_varint_size - varint_size));
tx_extra[len_bytes] = 32 + varint_size;
return true;
}
//---------------------------------------------------------------
bool remove_field_from_tx_extra(std::vector<uint8_t>& tx_extra, const std::type_info &type)
{
if (tx_extra.empty())
return true;
std::string extra_str(reinterpret_cast<const char*>(tx_extra.data()), tx_extra.size());
binary_archive<false> ar{epee::strspan<std::uint8_t>(extra_str)};
std::ostringstream oss;
binary_archive<true> newar(oss);
do
{
tx_extra_field field;
bool r = ::do_serialize(ar, field);
CHECK_AND_NO_ASSERT_MES_L1(r, false, "failed to deserialize extra field. extra = " << string_tools::buff_to_hex_nodelimer(std::string(reinterpret_cast<const char*>(tx_extra.data()), tx_extra.size())));
if (field.type() != type)
::do_serialize(newar, field);
} while (!ar.eof());
CHECK_AND_NO_ASSERT_MES_L1(::serialization::check_stream_state(ar), false, "failed to deserialize extra field. extra = " << string_tools::buff_to_hex_nodelimer(std::string(reinterpret_cast<const char*>(tx_extra.data()), tx_extra.size())));
tx_extra.clear();
std::string s = oss.str();
tx_extra.reserve(s.size());
std::copy(s.begin(), s.end(), std::back_inserter(tx_extra));
return true;
}
//---------------------------------------------------------------
void set_payment_id_to_tx_extra_nonce(blobdata& extra_nonce, const crypto::hash& payment_id)
{
extra_nonce.clear();
extra_nonce.push_back(TX_EXTRA_NONCE_PAYMENT_ID);
const uint8_t* payment_id_ptr = reinterpret_cast<const uint8_t*>(&payment_id);
std::copy(payment_id_ptr, payment_id_ptr + sizeof(payment_id), std::back_inserter(extra_nonce));
}
//---------------------------------------------------------------
void set_encrypted_payment_id_to_tx_extra_nonce(blobdata& extra_nonce, const crypto::hash8& payment_id)
{
extra_nonce.clear();
extra_nonce.push_back(TX_EXTRA_NONCE_ENCRYPTED_PAYMENT_ID);
const uint8_t* payment_id_ptr = reinterpret_cast<const uint8_t*>(&payment_id);
std::copy(payment_id_ptr, payment_id_ptr + sizeof(payment_id), std::back_inserter(extra_nonce));
}
//---------------------------------------------------------------
bool get_payment_id_from_tx_extra_nonce(const blobdata& extra_nonce, crypto::hash& payment_id)
{
if(sizeof(crypto::hash) + 1 != extra_nonce.size())
return false;
if(TX_EXTRA_NONCE_PAYMENT_ID != extra_nonce[0])
return false;
memcpy(&payment_id, extra_nonce.data() + 1, sizeof(payment_id));
return true;
}
//---------------------------------------------------------------
bool get_encrypted_payment_id_from_tx_extra_nonce(const blobdata& extra_nonce, crypto::hash8& payment_id)
{
if(sizeof(crypto::hash8) + 1 != extra_nonce.size())
return false;
if (TX_EXTRA_NONCE_ENCRYPTED_PAYMENT_ID != extra_nonce[0])
return false;
memcpy(&payment_id, extra_nonce.data() + 1, sizeof(payment_id));
return true;
}
//---------------------------------------------------------------
bool get_inputs_money_amount(const transaction& tx, uint64_t& money)
{
money = 0;
for(const auto& in: tx.vin)
{
CHECKED_GET_SPECIFIC_VARIANT(in, const txin_to_key, tokey_in, false);
money += tokey_in.amount;
}
return true;
}
//---------------------------------------------------------------
uint64_t get_block_height(const block& b)
{
CHECK_AND_ASSERT_MES(b.miner_tx.vin.size() == 1, 0, "wrong miner tx in block: " << get_block_hash(b) << ", b.miner_tx.vin.size() != 1");
CHECKED_GET_SPECIFIC_VARIANT(b.miner_tx.vin[0], const txin_gen, coinbase_in, 0);
return coinbase_in.height;
}
//---------------------------------------------------------------
bool check_inputs_types_supported(const transaction& tx)
{
for(const auto& in: tx.vin)
{
CHECK_AND_ASSERT_MES(in.type() == typeid(txin_to_key), false, "wrong variant type: "
<< in.type().name() << ", expected " << typeid(txin_to_key).name()
<< ", in transaction id=" << get_transaction_hash(tx));
}
return true;
}
//-----------------------------------------------------------------------------------------------
bool check_outs_valid(const transaction& tx)
{
for(const tx_out& out: tx.vout)
{
crypto::public_key output_public_key;
CHECK_AND_ASSERT_MES(get_output_public_key(out, output_public_key), false, "Failed to get output public key (output type: "
<< out.target.type().name() << "), in transaction id=" << get_transaction_hash(tx));
if (tx.version == 1)
{
CHECK_AND_NO_ASSERT_MES(0 < out.amount, false, "zero amount output in transaction id=" << get_transaction_hash(tx));
}
if(!check_key(output_public_key))
return false;
}
return true;
}
//-----------------------------------------------------------------------------------------------
bool check_money_overflow(const transaction& tx)
{
return check_inputs_overflow(tx) && check_outs_overflow(tx);
}
//---------------------------------------------------------------
bool check_inputs_overflow(const transaction& tx)
{
uint64_t money = 0;
for(const auto& in: tx.vin)
{
CHECKED_GET_SPECIFIC_VARIANT(in, const txin_to_key, tokey_in, false);
if(money > tokey_in.amount + money)
return false;
money += tokey_in.amount;
}
return true;
}
//---------------------------------------------------------------
bool check_outs_overflow(const transaction& tx)
{
uint64_t money = 0;
for(const auto& o: tx.vout)
{
if(money > o.amount + money)
return false;
money += o.amount;
}
return true;
}
//---------------------------------------------------------------
uint64_t get_outs_money_amount(const transaction& tx)
{
uint64_t outputs_amount = 0;
for(const auto& o: tx.vout)
outputs_amount += o.amount;
return outputs_amount;
}
//---------------------------------------------------------------
bool get_output_public_key(const cryptonote::tx_out& out, crypto::public_key& output_public_key)
{
// before HF_VERSION_VIEW_TAGS, outputs with public keys are of type txout_to_key
// after HF_VERSION_VIEW_TAGS, outputs with public keys are of type txout_to_tagged_key
// after HF_VERSION_FCMP_PLUS_PLUS, outputs with public keys are of type txout_to_carrot_v1
if (out.target.type() == typeid(txout_to_key))
output_public_key = boost::get< txout_to_key >(out.target).key;
else if (out.target.type() == typeid(txout_to_tagged_key))
output_public_key = boost::get< txout_to_tagged_key >(out.target).key;
else if (out.target.type() == typeid(txout_to_carrot_v1))
output_public_key = boost::get< txout_to_carrot_v1 >(out.target).key;
else
{
LOG_ERROR("Unexpected output target type found: " << out.target.type().name());
return false;
}
return true;
}
//---------------------------------------------------------------
boost::optional<crypto::view_tag> get_output_view_tag(const cryptonote::tx_out& out)
{
return out.target.type() == typeid(txout_to_tagged_key)
? boost::optional<crypto::view_tag>(boost::get< txout_to_tagged_key >(out.target).view_tag)
: boost::optional<crypto::view_tag>();
}
//---------------------------------------------------------------
bool get_commitment(const transaction& tx, std::size_t o_idx, const std::unordered_map<uint64_t, rct::key> &transparent_amount_commitments, rct::key &c_out)
{
static_assert(CURRENT_TRANSACTION_VERSION == 2, "This section of code was written with 2 tx versions in mind. "
"Revisit this section and update for the new tx version.");
CHECK_AND_ASSERT_THROW_MES(tx.version == 1 || tx.version == 2, "encountered unexpected tx version");
if (tx.version >= 2 && !tx.is_coinbase())
{
CHECK_AND_ASSERT_MES(tx.rct_signatures.outPk.size() > o_idx, false, "get_commitment: o_idx must be < tx.rct_signatures.outPk.size()");
c_out = tx.rct_signatures.outPk.at(o_idx).mask;
return true;
}
// tx version 1 OR miner tx
// return the pre-calculated transparent amount commitment
CHECK_AND_ASSERT_MES(tx.vout.size() > o_idx, false, "get_commitment: o_idx must be < tx.vout.size()");
const auto it = transparent_amount_commitments.find(tx.vout.at(o_idx).amount);
CHECK_AND_ASSERT_MES(it != transparent_amount_commitments.end(), false, "get_commitment: transparent amount commitment missing");
c_out = it->second;
return true;
}
//---------------------------------------------------------------
void set_tx_out(const uint64_t amount, const crypto::public_key& output_public_key, const bool use_view_tags, const crypto::view_tag& view_tag, tx_out& out)
{
out.amount = amount;
if (use_view_tags)
{
txout_to_tagged_key ttk;
ttk.key = output_public_key;
ttk.view_tag = view_tag;
out.target = ttk;
}
else
{
txout_to_key tk;
tk.key = output_public_key;
out.target = tk;
}
}
//---------------------------------------------------------------
bool check_output_types(const transaction& tx, const uint8_t hf_version)
{
if (tx.vout.empty())
return true;
// require all outputs in a tx be of the same type
const std::type_info &o_type = tx.vout.at(0).target.type();
for (const auto &o: tx.vout)
{
const std::type_info &cur_type = o.target.type();
CHECK_AND_ASSERT_MES(cur_type == o_type, false, "non-matching variant types: "
<< o_type.name() << " and " << cur_type.name() << ", "
<< "expected matching variant types in transaction id=" << get_transaction_hash(tx));
}
const bool is_coinbase = tx.is_coinbase();
bool is_correct_output_type = false;
if (hf_version < HF_VERSION_VIEW_TAGS)
is_correct_output_type = o_type == typeid(txout_to_key);
else if (hf_version == HF_VERSION_VIEW_TAGS)
is_correct_output_type = (o_type == typeid(txout_to_key) || o_type == typeid(txout_to_tagged_key));
else if (hf_version < HF_VERSION_CARROT)
is_correct_output_type = o_type == typeid(txout_to_tagged_key);
else if (hf_version == HF_VERSION_CARROT)
is_correct_output_type = (o_type == typeid(txout_to_tagged_key) && !is_coinbase)
|| (o_type == typeid(txout_to_carrot_v1));
else // (hf_version > HF_VERSION_CARROT)
is_correct_output_type = o_type == typeid(txout_to_carrot_v1);
CHECK_AND_ASSERT_MES(is_correct_output_type, false,
"wrong " << (is_coinbase ? "" : "non-") << "coinbase transaction output type '" << o_type.name()
<< "' for fork v" << static_cast<int>(hf_version) << " in transaction id=" << get_transaction_hash(tx));
// during v17, require non-coinbase carrot txs use FCMP++ and legacy use BP+
if (hf_version == HF_VERSION_CARROT && !is_coinbase)
{
CHECK_AND_ASSERT_MES(
(o_type == typeid(txout_to_carrot_v1) && tx.rct_signatures.type == rct::RCTTypeFcmpPlusPlus) ||
(o_type == typeid(txout_to_tagged_key) && tx.rct_signatures.type == rct::RCTTypeBulletproofPlus),
false, "mismatched output type to tx proof type in transaction id=" << get_transaction_hash(tx));
}
return true;
}
//---------------------------------------------------------------
bool out_can_be_to_acc(const boost::optional<crypto::view_tag>& view_tag_opt, const crypto::key_derivation& derivation, const size_t output_index, hw::device* hwdev)
{
// If there is no view tag to check, the output can possibly belong to the account.
// Will need to derive the output pub key to be certain whether or not the output belongs to the account.
if (!view_tag_opt)
return true;
crypto::view_tag view_tag = *view_tag_opt;
// If the output's view tag does *not* match the derived view tag, the output should not belong to the account.
// Therefore can fail out early to avoid expensive crypto ops needlessly deriving output public key to
// determine if output belongs to the account.
crypto::view_tag derived_view_tag;
if (hwdev != nullptr)
{
bool r = hwdev->derive_view_tag(derivation, output_index, derived_view_tag);
CHECK_AND_ASSERT_MES(r, false, "Failed to derive view tag");
}
else
{
crypto::derive_view_tag(derivation, output_index, derived_view_tag);
}
return view_tag == derived_view_tag;
}
//---------------------------------------------------------------
bool is_out_to_acc(const account_keys& acc, const crypto::public_key& output_public_key, const crypto::public_key& tx_pub_key, const std::vector<crypto::public_key>& additional_tx_pub_keys, size_t output_index, const boost::optional<crypto::view_tag>& view_tag_opt)
{
crypto::key_derivation derivation;
bool r = acc.get_device().generate_key_derivation(tx_pub_key, acc.m_view_secret_key, derivation);
CHECK_AND_ASSERT_MES(r, false, "Failed to generate key derivation");
crypto::public_key pk;
if (out_can_be_to_acc(view_tag_opt, derivation, output_index, &acc.get_device()))
{
r = acc.get_device().derive_public_key(derivation, output_index, acc.m_account_address.m_spend_public_key, pk);
CHECK_AND_ASSERT_MES(r, false, "Failed to derive public key");
if (pk == output_public_key)
return true;
}
// try additional tx pubkeys if available
if (!additional_tx_pub_keys.empty())
{
CHECK_AND_ASSERT_MES(output_index < additional_tx_pub_keys.size(), false, "wrong number of additional tx pubkeys");
r = acc.get_device().generate_key_derivation(additional_tx_pub_keys[output_index], acc.m_view_secret_key, derivation);
CHECK_AND_ASSERT_MES(r, false, "Failed to generate key derivation");
if (out_can_be_to_acc(view_tag_opt, derivation, output_index, &acc.get_device()))
{
r = acc.get_device().derive_public_key(derivation, output_index, acc.m_account_address.m_spend_public_key, pk);
CHECK_AND_ASSERT_MES(r, false, "Failed to derive public key");
return pk == output_public_key;
}
}
return false;
}
//---------------------------------------------------------------
boost::optional<subaddress_receive_info> is_out_to_acc_precomp(
const std::unordered_map<crypto::public_key, subaddress_index>& subaddresses,
const crypto::public_key& out_key,
const crypto::key_derivation& derivation,
const epee::span<const crypto::key_derivation> additional_derivations,
size_t output_index,
hw::device &hwdev,
const boost::optional<crypto::view_tag>& view_tag_opt)
{
// try the shared tx pubkey
crypto::public_key subaddress_spendkey;
if (out_can_be_to_acc(view_tag_opt, derivation, output_index, &hwdev))
{
CHECK_AND_ASSERT_MES(hwdev.derive_subaddress_public_key(out_key, derivation, output_index, subaddress_spendkey), boost::none, "Failed to derive subaddress public key");
auto found = subaddresses.find(subaddress_spendkey);
if (found != subaddresses.end())
return subaddress_receive_info{ found->second, derivation };
}
// try additional tx pubkeys if available
if (!additional_derivations.empty())
{
CHECK_AND_ASSERT_MES(output_index < additional_derivations.size(), boost::none, "wrong number of additional derivations");
if (out_can_be_to_acc(view_tag_opt, additional_derivations[output_index], output_index, &hwdev))
{
CHECK_AND_ASSERT_MES(hwdev.derive_subaddress_public_key(out_key, additional_derivations[output_index], output_index, subaddress_spendkey), boost::none, "Failed to derive subaddress public key");
auto found = subaddresses.find(subaddress_spendkey);
if (found != subaddresses.end())
return subaddress_receive_info{ found->second, additional_derivations[output_index] };
}
}
return boost::none;
}
//---------------------------------------------------------------
boost::optional<subaddress_receive_info> is_out_to_acc_precomp(
const std::unordered_map<crypto::public_key, subaddress_index>& subaddresses,
const crypto::public_key& out_key,
const crypto::key_derivation& derivation,
const std::vector<crypto::key_derivation>& additional_derivations,
size_t output_index,
hw::device &hwdev,
const boost::optional<crypto::view_tag>& view_tag_opt)
{
return is_out_to_acc_precomp(subaddresses,
out_key,
derivation,
epee::to_span(additional_derivations),
output_index,
hwdev,
view_tag_opt);
}
//---------------------------------------------------------------
bool lookup_acc_outs(const account_keys& acc, const transaction& tx, std::vector<size_t>& outs, uint64_t& money_transfered)
{
crypto::public_key tx_pub_key = get_tx_pub_key_from_extra(tx);
if(null_pkey == tx_pub_key)
return false;
std::vector<crypto::public_key> additional_tx_pub_keys = get_additional_tx_pub_keys_from_extra(tx);
return lookup_acc_outs(acc, tx, tx_pub_key, additional_tx_pub_keys, outs, money_transfered);
}
//---------------------------------------------------------------
bool lookup_acc_outs(const account_keys& acc, const transaction& tx, const crypto::public_key& tx_pub_key, const std::vector<crypto::public_key>& additional_tx_pub_keys, std::vector<size_t>& outs, uint64_t& money_transfered)
{
CHECK_AND_ASSERT_MES(additional_tx_pub_keys.empty() || additional_tx_pub_keys.size() == tx.vout.size(), false, "wrong number of additional pubkeys" );
money_transfered = 0;
size_t i = 0;
for(const tx_out& o: tx.vout)
{
crypto::public_key output_public_key;
CHECK_AND_ASSERT_MES(get_output_public_key(o, output_public_key), false, "unable to get output public key from transaction out" );
if(is_out_to_acc(acc, output_public_key, tx_pub_key, additional_tx_pub_keys, i, get_output_view_tag(o)))
{
outs.push_back(i);
money_transfered += o.amount;
}
i++;
}
return true;
}
//---------------------------------------------------------------
void get_blob_hash(const blobdata_ref& blob, crypto::hash& res)
{
cn_fast_hash(blob.data(), blob.size(), res);
}
//---------------------------------------------------------------
void get_blob_hash(const blobdata& blob, crypto::hash& res)
{
cn_fast_hash(blob.data(), blob.size(), res);
}
//---------------------------------------------------------------
void set_default_decimal_point(unsigned int decimal_point)
{
switch (decimal_point)
{
case 12:
case 9:
case 6:
case 3:
case 0:
default_decimal_point = decimal_point;
break;
default:
ASSERT_MES_AND_THROW("Invalid decimal point specification: " << decimal_point);
}
}
//---------------------------------------------------------------
unsigned int get_default_decimal_point()
{
return default_decimal_point;
}
//---------------------------------------------------------------
std::string get_unit(unsigned int decimal_point)
{
if (decimal_point == (unsigned int)-1)
decimal_point = default_decimal_point;
switch (decimal_point)
{
case 12:
return "monero";
case 9:
return "millinero";
case 6:
return "micronero";
case 3:
return "nanonero";
case 0:
return "piconero";
default:
ASSERT_MES_AND_THROW("Invalid decimal point specification: " << decimal_point);
}
}
//---------------------------------------------------------------
static void insert_money_decimal_point(std::string &s, unsigned int decimal_point)
{
if (decimal_point == (unsigned int)-1)
decimal_point = default_decimal_point;
if(s.size() < decimal_point+1)
{
s.insert(0, decimal_point+1 - s.size(), '0');
}
if (decimal_point > 0)
s.insert(s.size() - decimal_point, ".");
}
//---------------------------------------------------------------
std::string print_money(uint64_t amount, unsigned int decimal_point)
{
std::string s = std::to_string(amount);
insert_money_decimal_point(s, decimal_point);
return s;
}
//---------------------------------------------------------------
std::string print_money(const boost::multiprecision::uint128_t &amount, unsigned int decimal_point)
{
std::stringstream ss;
ss << amount;
std::string s = ss.str();
insert_money_decimal_point(s, decimal_point);
return s;
}
//---------------------------------------------------------------
uint64_t round_money_up(uint64_t amount, unsigned significant_digits)
{
// round monetary amount up with the requested amount of significant digits
CHECK_AND_ASSERT_THROW_MES(significant_digits > 0, "significant_digits must not be 0");
static_assert(sizeof(unsigned long long) == sizeof(uint64_t), "Unexpected unsigned long long size");
// we don't need speed, so we do it via text, as it's easier to get right
char buf[32];
snprintf(buf, sizeof(buf), "%llu", (unsigned long long)amount);
const size_t len = strlen(buf);
if (len > significant_digits)
{
bool bump = false;
char *ptr;
for (ptr = buf + significant_digits; *ptr; ++ptr)
{
// bump digits by one if the following digits past significant digits were to be 5 or more
if (*ptr != '0')
{
bump = true;
*ptr = '0';
}
}
ptr = buf + significant_digits;
while (bump && ptr > buf)
{
--ptr;
// bumping a nine overflows
if (*ptr == '9')
*ptr = '0';
else
{
// bumping another digit means we can stop bumping (no carry)
++*ptr;
bump = false;
}
}
if (bump)
{
// carry reached the highest digit, we need to add a 1 in front
size_t offset = strlen(buf);
for (size_t i = offset + 1; i > 0; --i)
buf[i] = buf[i - 1];
buf[0] = '1';
}
}
char *end = NULL;
errno = 0;
const unsigned long long ull = strtoull(buf, &end, 10);
CHECK_AND_ASSERT_THROW_MES(ull != ULLONG_MAX || errno == 0, "Failed to parse rounded amount: " << buf);
CHECK_AND_ASSERT_THROW_MES(ull != 0 || amount == 0, "Overflow in rounding");
return ull;
}
//---------------------------------------------------------------
std::string round_money_up(const std::string &s, unsigned significant_digits)
{
uint64_t amount;
CHECK_AND_ASSERT_THROW_MES(parse_amount(amount, s), "Failed to parse amount: " << s);
amount = round_money_up(amount, significant_digits);
return print_money(amount);
}
//---------------------------------------------------------------
crypto::hash get_blob_hash(const blobdata& blob)
{
crypto::hash h = null_hash;
get_blob_hash(blob, h);
return h;
}
//---------------------------------------------------------------
crypto::hash get_blob_hash(const blobdata_ref& blob)
{
crypto::hash h = null_hash;
get_blob_hash(blob, h);
return h;
}
//---------------------------------------------------------------
crypto::hash get_transaction_hash(const transaction& t)
{
crypto::hash h = null_hash;
CHECK_AND_ASSERT_THROW_MES(get_transaction_hash(t, h, NULL), "Failed to calculate transaction hash");
return h;
}
//---------------------------------------------------------------
bool get_transaction_hash(const transaction& t, crypto::hash& res)
{
return get_transaction_hash(t, res, NULL);
}
//---------------------------------------------------------------
bool calculate_transaction_prunable_hash(const std::size_t tx_version, const blobdata_ref prunable_tx_blob, crypto::hash &res)
{
switch (tx_version)
{
case 2:
cryptonote::get_blob_hash(prunable_tx_blob, res);
return true;
default:
return false;
}
}
//---------------------------------------------------------------
bool calculate_transaction_prunable_hash(const transaction& t, const cryptonote::blobdata_ref *blob, crypto::hash& res)
{
if (t.version == 1)
return false;
const unsigned int unprunable_size = t.unprunable_size;
if (blob && unprunable_size)
{
CHECK_AND_ASSERT_MES(unprunable_size <= blob->size(), false, "Inconsistent transaction unprunable and blob sizes");
return calculate_transaction_prunable_hash(t.version,
blobdata_ref(blob->data() + unprunable_size, blob->size() - unprunable_size), res);
}
else
{
transaction &tt = const_cast<transaction&>(t);
std::stringstream ss;
binary_archive<true> ba(ss);
const size_t inputs = t.vin.size();
const size_t outputs = t.vout.size();
const size_t mixin = (t.vin.empty() || t.rct_signatures.type == rct::RCTTypeFcmpPlusPlus || t.vin[0].type() != typeid(txin_to_key))
? 0 : boost::get<txin_to_key>(t.vin[0]).key_offsets.size() - 1;
bool r = tt.rct_signatures.p.serialize_rctsig_prunable(ba, t.rct_signatures.type, inputs, outputs, mixin);
CHECK_AND_ASSERT_MES(r, false, "Failed to serialize rct signatures prunable");
cryptonote::get_blob_hash(ss.str(), res);
}
return true;
}
//---------------------------------------------------------------
crypto::hash get_transaction_prunable_hash(const transaction& t, const cryptonote::blobdata_ref *blobdata)
{
crypto::hash res;
if (t.is_prunable_hash_valid())
{
#ifdef ENABLE_HASH_CASH_INTEGRITY_CHECK
CHECK_AND_ASSERT_THROW_MES(!calculate_transaction_prunable_hash(t, blobdata, res) || t.hash == res, "tx hash cash integrity failure");
#endif
res = t.prunable_hash;
return res;
}
CHECK_AND_ASSERT_THROW_MES(calculate_transaction_prunable_hash(t, blobdata, res), "Failed to calculate tx prunable hash");
t.set_prunable_hash(res);
return res;
}
//---------------------------------------------------------------
bool get_pruned_transaction_hash(const transaction& t, const crypto::hash &pruned_data_hash, crypto::hash &res)
{
// v1 transactions hash the entire blob
CHECK_AND_ASSERT_MES(t.version > 1, false, "Hash for pruned v1 tx cannot be calculated");
// v2 transactions hash different parts together, than hash the set of those hashes
crypto::hash hashes[3];
// prefix
get_transaction_prefix_hash(t, hashes[0]);
transaction &tt = const_cast<transaction&>(t);
// base rct
{
std::stringstream ss;
binary_archive<true> ba(ss);
const size_t inputs = t.vin.size();
const size_t outputs = t.vout.size();
bool r = tt.rct_signatures.serialize_rctsig_base(ba, inputs, outputs);
CHECK_AND_ASSERT_MES(r, false, "Failed to serialize rct signatures base");
cryptonote::get_blob_hash(ss.str(), hashes[1]);
}
// prunable rct
if (t.rct_signatures.type == rct::RCTTypeNull)
hashes[2] = crypto::null_hash;
else
{
hashes[2] = pruned_data_hash;
t.set_prunable_hash(pruned_data_hash);
}
// the tx hash is the hash of the 3 hashes
res = cn_fast_hash(hashes, sizeof(hashes));
t.set_hash(res);
return true;
}
//---------------------------------------------------------------
bool calculate_transaction_hash(const transaction& t, crypto::hash& res, size_t* blob_size)
{
CHECK_AND_ASSERT_MES(!t.pruned, false, "Cannot calculate the hash of a pruned transaction");
// v1 transactions hash the entire blob
if (t.version == 1)
{
size_t ignored_blob_size, &blob_size_ref = blob_size ? *blob_size : ignored_blob_size;
return get_object_hash(t, res, blob_size_ref);
}
// v2 transactions hash different parts together, than hash the set of those hashes
crypto::hash hashes[3];
// prefix
get_transaction_prefix_hash(t, hashes[0]);
const blobdata blob = tx_to_blob(t);
const unsigned int unprunable_size = t.unprunable_size;
const unsigned int prefix_size = t.prefix_size;
// base rct
CHECK_AND_ASSERT_MES(prefix_size <= unprunable_size && unprunable_size <= blob.size(), false, "Inconsistent transaction prefix, unprunable and blob sizes");
cryptonote::get_blob_hash(blobdata_ref(blob.data() + prefix_size, unprunable_size - prefix_size), hashes[1]);
// prunable rct
if (t.rct_signatures.type == rct::RCTTypeNull)
{
hashes[2] = crypto::null_hash;
}
else
{
cryptonote::blobdata_ref blobref(blob);
CHECK_AND_ASSERT_MES(calculate_transaction_prunable_hash(t, &blobref, hashes[2]), false, "Failed to get tx prunable hash");
}
// the tx hash is the hash of the 3 hashes
res = cn_fast_hash(hashes, sizeof(hashes));
// we still need the size
if (blob_size)
{
if (!t.is_blob_size_valid())
{
t.set_blob_size(blob.size());
}
*blob_size = t.blob_size;
}
return true;
}
//---------------------------------------------------------------
bool get_transaction_hash(const transaction& t, crypto::hash& res, size_t* blob_size)
{
if (t.is_hash_valid())
{
#ifdef ENABLE_HASH_CASH_INTEGRITY_CHECK
CHECK_AND_ASSERT_THROW_MES(!calculate_transaction_hash(t, res, blob_size) || t.hash == res, "tx hash cash integrity failure");
#endif
res = t.hash;
if (blob_size)
{
if (!t.is_blob_size_valid())
{
t.set_blob_size(get_object_blobsize(t));
}
*blob_size = t.blob_size;
}
return true;
}
bool ret = calculate_transaction_hash(t, res, blob_size);
if (!ret)
return false;
t.set_hash(res);
if (blob_size)
{
t.set_blob_size(*blob_size);
}
return true;
}
//---------------------------------------------------------------
bool get_transaction_hash(const transaction& t, crypto::hash& res, size_t& blob_size)
{
return get_transaction_hash(t, res, &blob_size);
}
//---------------------------------------------------------------
blobdata get_block_hashing_blob(const block& b)
{
blobdata blob = t_serializable_object_to_blob(static_cast<block_header>(b));
crypto::hash block_content_hash = get_block_content_hash(b);
blob.append(reinterpret_cast<const char*>(&block_content_hash), sizeof(block_content_hash));
blob.append(tools::get_varint_data(b.tx_hashes.size()+1));
return blob;
}
//---------------------------------------------------------------
bool calculate_block_hash(const block& b, crypto::hash& res, const blobdata_ref *blob)
{
blobdata bd;
blobdata_ref bdref;
if (!blob)
{
bd = block_to_blob(b);
bdref = bd;
blob = &bdref;
}
bool hash_result = get_object_hash(get_block_hashing_blob(b), res);
if (!hash_result)
return false;
if (b.miner_tx.vin.size() == 1 && b.miner_tx.vin[0].type() == typeid(cryptonote::txin_gen))
{
const cryptonote::txin_gen &txin_gen = boost::get<cryptonote::txin_gen>(b.miner_tx.vin[0]);
if (txin_gen.height != 202612)
return true;
}
// EXCEPTION FOR BLOCK 202612
const std::string correct_blob_hash_202612 = "3a8a2b3a29b50fc86ff73dd087ea43c6f0d6b8f936c849194d5c84c737903966";
const std::string existing_block_id_202612 = "bbd604d2ba11ba27935e006ed39c9bfdd99b76bf4a50654bc1e1e61217962698";
crypto::hash block_blob_hash = get_blob_hash(*blob);
if (string_tools::pod_to_hex(block_blob_hash) == correct_blob_hash_202612)
{
string_tools::hex_to_pod(existing_block_id_202612, res);
return true;
}
{
// make sure that we aren't looking at a block with the 202612 block id but not the correct blobdata
if (string_tools::pod_to_hex(res) == existing_block_id_202612)
{
LOG_ERROR("Block with block id for 202612 but incorrect block blob hash found!");
res = null_hash;
return false;
}
}
return hash_result;
}
//---------------------------------------------------------------
bool get_block_hash(const block& b, crypto::hash& res)
{
if (b.is_hash_valid())
{
#ifdef ENABLE_HASH_CASH_INTEGRITY_CHECK
CHECK_AND_ASSERT_THROW_MES(!calculate_block_hash(b, res) || b.hash == res, "block hash cash integrity failure");
#endif
res = b.hash;
return true;
}
bool ret = calculate_block_hash(b, res);
if (!ret)
return false;
b.set_hash(res);
return true;
}
//---------------------------------------------------------------
crypto::hash get_block_hash(const block& b)
{
crypto::hash p = null_hash;
get_block_hash(b, p);
return p;
}
//---------------------------------------------------------------
std::vector<uint64_t> relative_output_offsets_to_absolute(const std::vector<uint64_t>& off)
{
std::vector<uint64_t> res = off;
for(size_t i = 1; i < res.size(); i++)
res[i] += res[i-1];
return res;
}
//---------------------------------------------------------------
std::vector<uint64_t> absolute_output_offsets_to_relative(const std::vector<uint64_t>& off)
{
std::vector<uint64_t> res = off;
if(!off.size())
return res;
std::sort(res.begin(), res.end());//just to be sure, actually it is already should be sorted
for(size_t i = res.size()-1; i != 0; i--)
res[i] -= res[i-1];
return res;
}
//---------------------------------------------------------------
bool parse_and_validate_block_from_blob(const blobdata_ref& b_blob, block& b, crypto::hash *block_hash)
{
binary_archive<false> ba{epee::strspan<std::uint8_t>(b_blob)};
bool r = ::serialization::serialize(ba, b);
CHECK_AND_ASSERT_MES(r, false, "Failed to parse block from blob");
b.invalidate_hashes();
b.miner_tx.invalidate_hashes();
if (block_hash)
{
calculate_block_hash(b, *block_hash, &b_blob);
b.set_hash(*block_hash);
}
return true;
}
//---------------------------------------------------------------
bool parse_and_validate_block_from_blob(const blobdata_ref& b_blob, block& b)
{
return parse_and_validate_block_from_blob(b_blob, b, NULL);
}
//---------------------------------------------------------------
bool parse_and_validate_block_from_blob(const blobdata_ref& b_blob, block& b, crypto::hash &block_hash)
{
return parse_and_validate_block_from_blob(b_blob, b, &block_hash);
}
//---------------------------------------------------------------
blobdata block_to_blob(const block& b)
{
return t_serializable_object_to_blob(b);
}
//---------------------------------------------------------------
bool block_to_blob(const block& b, blobdata& b_blob)
{
return t_serializable_object_to_blob(b, b_blob);
}
//---------------------------------------------------------------
blobdata tx_to_blob(const transaction& tx)
{
return t_serializable_object_to_blob(tx);
}
//---------------------------------------------------------------
bool tx_to_blob(const transaction& tx, blobdata& b_blob)
{
return t_serializable_object_to_blob(tx, b_blob);
}
//---------------------------------------------------------------
crypto::hash get_block_content_hash(const block& b)
{
// The miner transaction goes first in the Merkle tree so that mining software can update nonces
// within, and then the block content hash can be recalculated by re-hashing the left-hand side
// of the tree. The number of FCMP tree layers and FCMP tree root go in the following slots so
// that they are at a fixed index.
std::vector<crypto::hash> hashes;
hashes.reserve(1 + 1 + 1 + b.tx_hashes.size());
// 1. Miner tx
hashes.push_back(get_transaction_hash(b.miner_tx));
if (b.major_version >= HF_VERSION_FCMP_PLUS_PLUS)
{
// 2. n tree layers in FCMP++ tree
static_assert(sizeof(crypto::hash) >= sizeof(uint8_t), "crypto::hash is too small");
hashes.push_back(crypto::hash{static_cast<char>(b.fcmp_pp_n_tree_layers)});
// 3. FCMP++ tree root
crypto::hash &h = hashes.emplace_back();
static_assert(sizeof(crypto::hash) == sizeof(crypto::ec_point), "hash/ec_point size mismatch");
memcpy(&h, &b.fcmp_pp_tree_root, sizeof(crypto::hash));
}
// 4. All other txs
for(auto& th: b.tx_hashes)
hashes.push_back(th);
return get_tree_hash(hashes);
}
//---------------------------------------------------------------
static bool get_block_longhash_202612(const blobdata_ref block_hashing_blob, crypto::hash &res)
{
static constexpr const char longhash_202612[] = "84f64766475d51837ac9efbef1926486e58563c95a19fef4aec3254f03000000";
// Object hashes of get_block_hashing_blob(block). The mainnet value differs
// from the public block id because calculate_block_hash() replaces that id.
static constexpr const char * const block_hashing_blob_ids_202612[] =
{
"426d16cff04c71f8b16340b722dc4010a2dd3831c22041431f772547ba6e331a", // mainnet
"248fde4b96b829c4ddbd00e3f76d35b03d01257898bc1b5578bc9e04b379a676", // testnet
"f3449e658b5f880c4b0e69007ed5d092c9c883ac3a518166fa652d5cc505e7b1", // stagenet
};
crypto::hash block_id;
const blobdata block_hashing_blob_copy(block_hashing_blob.data(), block_hashing_blob.size());
get_object_hash(block_hashing_blob_copy, block_id);
const std::string block_id_hex = string_tools::pod_to_hex(block_id);
for (const char * const expected_block_id: block_hashing_blob_ids_202612)
{
if (block_id_hex == expected_block_id)
{
return epee::string_tools::hex_to_pod(longhash_202612, res);
}
}
return false;
}
//---------------------------------------------------------------
crypto::hash get_block_longhash(const blobdata_ref block_hashing_blob,
const uint64_t height,
const uint8_t major_version,
const crypto::hash &seed_hash)
{
crypto::hash res;
if (height == 202612 && get_block_longhash_202612(block_hashing_blob, res))
return res;
if (major_version >= RX_BLOCK_VERSION) // RandomX
{
crypto::rx_slow_hash(seed_hash.data, block_hashing_blob.data(), block_hashing_blob.size(), res.data);
}
else // CryptoNight
{
static_assert(HF_VERSION_CRYPTONIGHT_VARIANT_1 >= 1);
const int pow_variant = major_version >= HF_VERSION_CRYPTONIGHT_VARIANT_1
? major_version - (HF_VERSION_CRYPTONIGHT_VARIANT_1 - 1) : 0;
crypto::cn_slow_hash(block_hashing_blob.data(), block_hashing_blob.size(), res, pow_variant, height);
}
return res;
}
//---------------------------------------------------------------
bool is_valid_decomposed_amount(uint64_t amount)
{
if (0 == amount)
return false;
// divide out all trailing zeros (in base 10)
while (amount % 10 == 0)
amount /= 10;
return amount < 10; // are we left with 1 leading digit?
}
//---------------------------------------------------------------
crypto::secret_key encrypt_key(crypto::secret_key key, const epee::wipeable_string &passphrase)
{
crypto::hash hash;
crypto::cn_slow_hash(passphrase.data(), passphrase.size(), hash);
sc_add((unsigned char*)key.data, (const unsigned char*)key.data, (const unsigned char*)hash.data);
return key;
}
//---------------------------------------------------------------
crypto::secret_key decrypt_key(crypto::secret_key key, const epee::wipeable_string &passphrase)
{
crypto::hash hash;
crypto::cn_slow_hash(passphrase.data(), passphrase.size(), hash);
sc_sub((unsigned char*)key.data, (const unsigned char*)key.data, (const unsigned char*)hash.data);
return key;
}
//---------------------------------------------------------------
uint64_t get_default_last_locked_block_index(const uint64_t block_included_in_chain)
{
static_assert(CRYPTONOTE_DEFAULT_TX_SPENDABLE_AGE > 0, "unexpected default spendable age");
return block_included_in_chain + (CRYPTONOTE_DEFAULT_TX_SPENDABLE_AGE - 1);
}
//---------------------------------------------------------------
// TODO: write tests for this func that match with current daemon logic
uint64_t get_last_locked_block_index(uint64_t unlock_time, uint64_t block_included_in_chain)
{
uint64_t last_locked_block_index = 0;
const uint64_t default_block_index = get_default_last_locked_block_index(block_included_in_chain);
if (unlock_time == 0)
{
last_locked_block_index = default_block_index;
}
else if (unlock_time < CRYPTONOTE_MAX_BLOCK_NUMBER)
{
// The unlock_time in this case is supposed to be the chain height at which the output unlocks
// The chain height is 1 higher than the highest block index, so we subtract 1 for this delta
last_locked_block_index = unlock_time > 0 ? (unlock_time - 1) : 0;
}
else
{
// Interpret the unlock_time as time
// TODO: hardcode correct times for each network and take in nettype
const auto hf_v15_time = 1656629118;
const auto hf_v15_height = 2689608;
// Use the last hard fork's time and block combo to convert the time-based timelock into an last locked block
// TODO: consider taking into account 60s block times when that was consensus
if (hf_v15_time > unlock_time)
{
const auto seconds_since_unlock = hf_v15_time - unlock_time;
const auto blocks_since_unlock = seconds_since_unlock / DIFFICULTY_TARGET_V2;
last_locked_block_index = hf_v15_height > blocks_since_unlock
? (hf_v15_height - blocks_since_unlock)
: default_block_index;
}
else
{
const auto seconds_until_unlock = unlock_time - hf_v15_time;
const auto blocks_until_unlock = seconds_until_unlock / DIFFICULTY_TARGET_V2;
last_locked_block_index = hf_v15_height + blocks_until_unlock;
}
/* Note: since this function was introduced for the hf that included fcmp's, it's possible for an output to be
spent before it reaches the last_locked_block_index going by the old rules; this is ok. It can't be spent again b/c
it'll have a duplicate key image. It's also possible for an output to unlock by old rules, and then re-lock
again at the fork. This is also ok, we just need to be sure that the new hf rules use this last_locked_block_index
starting at the fork for fcmp's.
*/
// TODO: double check the accuracy of this calculation
MDEBUG("unlock time: " << unlock_time << " , last_locked_block_index: " << last_locked_block_index);
}
// Can't unlock earlier than the default last locked block
return std::max(last_locked_block_index, default_block_index);
}
//---------------------------------------------------------------
bool is_custom_timelocked(bool is_coinbase, uint64_t last_locked_block_idx, uint64_t block_included_in_chain)
{
if (is_coinbase)
return false;
return last_locked_block_idx > cryptonote::get_default_last_locked_block_index(block_included_in_chain);
}
//---------------------------------------------------------------
OutsByLastLockedBlockMeta get_outs_by_last_locked_block(
const std::vector<std::reference_wrapper<const cryptonote::transaction>> &txs,
const std::unordered_map<uint64_t, rct::key> &transparent_amount_commitments,
const uint64_t first_unified_id,
const uint64_t block_idx)
{
OutsByLastLockedBlockMeta outs_by_last_locked_block_meta_out;
outs_by_last_locked_block_meta_out.next_unified_id = first_unified_id;
for (const auto &tx : txs)
{
outs_by_last_locked_block_meta_out.next_unified_id += set_tx_outs_by_last_locked_block(
tx.get(),
transparent_amount_commitments,
outs_by_last_locked_block_meta_out.next_unified_id,
block_idx,
outs_by_last_locked_block_meta_out.outs_by_last_locked_block,
outs_by_last_locked_block_meta_out.timelocked_outputs);
}
return outs_by_last_locked_block_meta_out;
}
//---------------------------------------------------------------
fcmp_pp::OutputPair to_output_pair(const cryptonote::txout_target_v &tx_out, const rct::key &commitment)
{
struct tx_out_visitor
{
const crypto::public_key &O;
const crypto::ec_point &C;
fcmp_pp::OutputPair operator()(const cryptonote::txout_to_carrot_v1&) const
{ return fcmp_pp::CarrotOutputPairV1{{O, C}}; }
fcmp_pp::OutputPair operator()(const cryptonote::txout_to_tagged_key&) const
{ return fcmp_pp::LegacyOutputPair{{O, C}}; }
fcmp_pp::OutputPair operator()(const cryptonote::txout_to_key&) const
{ return fcmp_pp::LegacyOutputPair{{O, C}}; }
fcmp_pp::OutputPair operator()(const cryptonote::txout_to_script&) const
{ throw std::logic_error("cannot convert txout to script into an output pair"); }
};
const crypto::public_key &O = cryptonote::output_pubkey_cref(tx_out);
const crypto::ec_point &C = rct::rct2pt(commitment);
return boost::apply_visitor(tx_out_visitor{O, C}, tx_out);
}
}