Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -330,15 +330,18 @@ set(
src/common/monero_rpc_connection.cpp
src/utils/gen_utils.cpp
src/utils/monero_utils.cpp
src/utils/monero_wallet_utils.cpp
src/daemon/monero_daemon_model.cpp
src/daemon/monero_daemon.cpp
src/daemon/monero_daemon_rpc_model.cpp
src/daemon/monero_daemon_rpc.cpp
src/wallet/monero_wallet_model.cpp
src/wallet/monero_wallet_keys.cpp
src/wallet/monero_wallet_rpc_model.cpp
src/wallet/monero_wallet_light_model.cpp
src/wallet/monero_wallet_rpc.cpp
src/wallet/monero_wallet_full.cpp
src/wallet/monero_wallet_light.cpp
)

if (BUILD_LIBRARY)
Expand Down Expand Up @@ -446,13 +449,16 @@ endif()
DESTINATION include/daemon)
INSTALL(FILES src/utils/gen_utils.h
src/utils/monero_utils.h
src/utils/monero_wallet_utils.h
DESTINATION include/utils)
INSTALL(FILES src/wallet/monero_wallet_full.h
src/wallet/monero_wallet_rpc.h
src/wallet/monero_wallet_light.h
src/wallet/monero_wallet.h
src/wallet/monero_wallet_keys.h
src/wallet/monero_wallet_model.h
src/wallet/monero_wallet_rpc_model.h
src/wallet/monero_wallet_light_model.h
DESTINATION include/wallet)
INSTALL(TARGETS monero-cpp monero-cpp-static
RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR} COMPONENT Runtime
Expand Down
8 changes: 8 additions & 0 deletions src/common/monero_error.h
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,14 @@ namespace monero {
}
};

/**
* Exception when a derived one-time output key does not match the output's claimed public key.
*/
class monero_output_ownership_error : public monero_error {
public:
monero_output_ownership_error() { message = "Derived one-time output key does not match the output's public key"; }
};

/**
* Exception when interacting with the Monero daemon or wallet RPC API.
*/
Expand Down
58 changes: 58 additions & 0 deletions src/utils/gen_utils.h
Original file line number Diff line number Diff line change
Expand Up @@ -67,8 +67,13 @@
#include <chrono>
#include <thread>
#include <atomic>
#include <cstdlib>
#include <cstdio>
#include <cctype>
#include <algorithm>
#include "include_base_utils.h"
#include "common/util.h"
#include "crypto/crypto.h"

/**
* Collection of generic utilities.
Expand All @@ -87,6 +92,39 @@ namespace gen_utils
return boost::uuids::to_string(uuid);
}

static bool is_uint64_t(const std::string& str) {
if (str.empty() || !std::all_of(str.begin(), str.end(), [](unsigned char c) { return std::isdigit(c) != 0; })) return false;
errno = 0;
char* end = nullptr;
std::strtoull(str.c_str(), &end, 10);
if (errno == ERANGE) return false;
return end == str.c_str() + str.size();
}

static uint64_t uint64_t_cast(const std::string& str) {
if (!is_uint64_t(str)) throw std::out_of_range("String provided is not a valid uint64_t");
return static_cast<uint64_t>(std::strtoull(str.c_str(), nullptr, 10));
}

// based on Howard Hinnant's days_from_civil algorithm (http://howardhinnant.github.io/date_algorithms.html)
static uint64_t timestamp_to_epoch(const std::string& iso_timestamp) {
int year, month, day, hour, minute, second;
if (std::sscanf(iso_timestamp.c_str(), "%d-%d-%d%*[T ]%d:%d:%d", &year, &month, &day, &hour, &minute, &second) != 6) {
throw std::runtime_error("Invalid ISO 8601 timestamp: " + iso_timestamp);
}

int64_t y = year - (month <= 2 ? 1 : 0);
int64_t era = (y >= 0 ? y : y - 399) / 400;
uint64_t yoe = static_cast<uint64_t>(y - era * 400);
uint64_t doy = (153 * (month + (month > 2 ? -3 : 9)) + 2) / 5 + day - 1;
uint64_t doe = yoe * 365 + yoe / 4 - yoe / 100 + doy;
int64_t days = era * 146097 + static_cast<int64_t>(doe) - 719468;

int64_t seconds = days * 86400 + hour * 3600 + minute * 60 + second;
if (seconds < 0) throw std::out_of_range("Timestamp is before the Unix epoch");
return static_cast<uint64_t>(seconds);
}

/**
* Wait for the given duration.
*
Expand Down Expand Up @@ -179,6 +217,26 @@ namespace gen_utils
throw std::runtime_error("Cannot reconcile vectors" + (!err_msg.empty() ? std::string(". ") + err_msg : std::string("")));
}

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);
}

// ------------------------- THREAD POLLER ----------------------------

// WARNING: Only destroy a thread_poller from a thread other than its own pool thread.
Expand Down
86 changes: 82 additions & 4 deletions src/utils/monero_utils.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -601,10 +601,16 @@ bool monero_utils::vout_before(const std::shared_ptr<monero_output>& o1, const s
if (tx_height_less_than(ow1->m_tx, ow2->m_tx)) return true;

// compare by account index, subaddress index, output index, then key image hex
if (ow1->m_account_index.get() < ow2->m_account_index.get()) return true;
if (ow1->m_account_index.get() == ow2->m_account_index.get()) {
if (ow1->m_subaddress_index.get() < ow2->m_subaddress_index.get()) return true;
if (ow1->m_subaddress_index.get() == ow2->m_subaddress_index.get()) {
static const uint32_t EXTERNAL_SENTINEL = std::numeric_limits<uint32_t>::max();
uint32_t account1 = ow1->m_account_index.value_or(EXTERNAL_SENTINEL);
uint32_t account2 = ow2->m_account_index.value_or(EXTERNAL_SENTINEL);

if (account1 < account2) return true;
if (account1 == account2) {
uint32_t subaddress1 = ow1->m_subaddress_index.value_or(EXTERNAL_SENTINEL);
uint32_t subaddress2 = ow2->m_subaddress_index.value_or(EXTERNAL_SENTINEL);
if (subaddress1 < subaddress2) return true;
if (subaddress1 == subaddress2) {
if (ow1->m_index.get() < ow2->m_index.get()) return true;
if (ow1->m_index.get() == ow2->m_index.get()) throw std::runtime_error("Should never sort outputs with duplicate indices");
}
Expand Down Expand Up @@ -791,3 +797,75 @@ bool monero_utils::parse_payment_id_short(const std::string& payment_id_str, cry
payment_id = *reinterpret_cast<const crypto::hash8*>(payment_id_data.data());
return true;
}

std::shared_ptr<monero_key_image> monero_utils::generate_key_image(const crypto::public_key &ephem_pubkey, const size_t tx_output_index, const cryptonote::subaddress_index &received_subaddr, const cryptonote::account_base& account, const boost::optional<crypto::public_key>& expected_output_pubkey) {
// - R: ephem_pubkey
// - a: ack.m_view_secret_key [private viewkey]
// - b: ack.m_spend_secret_key [private spendkey]
// - idx: tx_output_index
// - index_major: received_subaddr.major
// - index_minor: received_subaddr.minor
// - Hs() [hash-to-scalar]
// - Hp() [hash-to-point]

const cryptonote::account_keys &ack = account.get_keys();
hw::device &hwdev = account.get_device();

// 1. Diffie-Helman derived secret D = a R
crypto::key_derivation recv_derivation;
CHECK_AND_ASSERT_THROW_MES(hwdev.generate_key_derivation(ephem_pubkey, ack.m_view_secret_key, recv_derivation), "Failed to perform Diffie-Helman exchange against tx ephem pubkey");

// 2. Non-address-extended onetime key secret u = Hs(D || idx) + b
crypto::secret_key onetime_privkey_unextended;
hwdev.derive_secret_key(recv_derivation, tx_output_index, ack.m_spend_secret_key, onetime_privkey_unextended);

// 3. Subaddress key extension s = Hs(a || index_major || index_minor) if is subaddress, else s = 0
const crypto::secret_key subaddr_ext{received_subaddr.is_zero() ? crypto::secret_key{} : hwdev.get_subaddress_secret_key(ack.m_view_secret_key, received_subaddr)};

// 4. Onetime address private key x = u + s
crypto::secret_key onetime_privkey;
hwdev.sc_secret_add(onetime_privkey, onetime_privkey_unextended, subaddr_ext);

// 5. Onetime address K = x G
crypto::public_key onetime_pubkey;
CHECK_AND_ASSERT_THROW_MES(hwdev.secret_key_to_public_key(onetime_privkey, onetime_pubkey), "Failed to make public key");

if (expected_output_pubkey != boost::none && onetime_pubkey != *expected_output_pubkey) throw monero_output_ownership_error();

// 6. Key image I = x Hp(K)
crypto::key_image ki;
hwdev.generate_key_image(onetime_pubkey, onetime_privkey, ki);

// sign the key image with the output secret key
crypto::signature signature;
std::vector<const crypto::public_key*> key_ptrs;
key_ptrs.push_back(&onetime_pubkey);

crypto::generate_ring_signature((const crypto::hash&)ki, ki, key_ptrs, onetime_privkey, 0, &signature);

std::shared_ptr<monero_key_image> key_image = std::make_shared<monero_key_image>();
key_image->m_hex = epee::string_tools::pod_to_hex(ki);
key_image->m_signature = epee::string_tools::pod_to_hex(signature);
return key_image;
}

void monero_utils::verify_output_ownership(const crypto::public_key &ephem_pubkey, const size_t tx_output_index, const cryptonote::subaddress_index &received_subaddr, const cryptonote::account_base& account, const crypto::public_key &expected_output_pubkey) {
const cryptonote::account_keys &ack = account.get_keys();
hw::device &hwdev = account.get_device();

crypto::key_derivation recv_derivation;
CHECK_AND_ASSERT_THROW_MES(hwdev.generate_key_derivation(ephem_pubkey, ack.m_view_secret_key, recv_derivation), "Failed to perform Diffie-Helman exchange against tx ephem pubkey");

crypto::secret_key onetime_privkey_unextended;
hwdev.derive_secret_key(recv_derivation, tx_output_index, ack.m_spend_secret_key, onetime_privkey_unextended);

const crypto::secret_key subaddr_ext{received_subaddr.is_zero() ? crypto::secret_key{} : hwdev.get_subaddress_secret_key(ack.m_view_secret_key, received_subaddr)};

crypto::secret_key onetime_privkey;
hwdev.sc_secret_add(onetime_privkey, onetime_privkey_unextended, subaddr_ext);

crypto::public_key onetime_pubkey;
CHECK_AND_ASSERT_THROW_MES(hwdev.secret_key_to_public_key(onetime_privkey, onetime_pubkey), "Failed to make public key");

if (onetime_pubkey != expected_output_pubkey) throw monero_output_ownership_error();
}
47 changes: 43 additions & 4 deletions src/utils/monero_utils.h
Original file line number Diff line number Diff line change
Expand Up @@ -55,8 +55,10 @@
#ifndef monero_utils_h
#define monero_utils_h

#include "common/monero_error.h"
#include "wallet/monero_wallet_model.h"
#include "cryptonote_basic/cryptonote_basic.h"
#include "cryptonote_core/cryptonote_tx_utils.h"
#include "serialization/keyvalue_serialization.h" // TODO: consolidate with other binary deps?
#include "storages/portable_storage.h"

Expand Down Expand Up @@ -219,15 +221,39 @@ namespace monero_utils
bool tx_height_less_than(const std::shared_ptr<monero_tx>& tx1, const std::shared_ptr<monero_tx>& tx2);

/**
* Returns true iff transfer1 is ordered before transfer2 by ascending account and subaddress indices.
*/
* Returns true iff transfer1 is ordered before transfer2 by ascending account and subaddress indices.
*/
bool incoming_transfer_before(const std::shared_ptr<monero_incoming_transfer>& transfer1, const std::shared_ptr<monero_incoming_transfer>& transfer2);

/**
* Returns true iff wallet vout1 is ordered before vout2 by ascending account and subaddress indices then index.
*/
* Returns true iff wallet vout1 is ordered before vout2 by ascending account and subaddress indices then index.
*/
bool vout_before(const std::shared_ptr<monero_output>& o1, const std::shared_ptr<monero_output>& o2);

/**
* Generates a key image for an output note (enote) in a simplified manner.
*
* @param ephem_pubkey is the tx main pubkey or an additional pubkey
* @param tx_output_index is the index of the enote in the local output set of the tx
* @param received_subaddr is the index of the recipient's subaddress
* @param account recipient's account
* @param expected_output_pubkey is the output's actual public key, to verify against the derived one-time key when supplied (boost::none skips the check for callers with no output public key to check against)
* @return the generated key image
*/
std::shared_ptr<monero_key_image> generate_key_image(const crypto::public_key &ephem_pubkey, const size_t tx_output_index, const cryptonote::subaddress_index &received_subaddr, const cryptonote::account_base& account, const boost::optional<crypto::public_key>& expected_output_pubkey);

/**
* Derives the one-time output public key and throws if it does not match expected_output_pubkey,
* without generating or signing a key image.
*
* @param ephem_pubkey is the tx main pubkey or an additional pubkey
* @param tx_output_index is the index of the enote in the local output set of the tx
* @param received_subaddr is the index of the recipient's subaddress
* @param account recipient's account
* @param expected_output_pubkey is the output's actual public key, to verify against the derived one-time key
*/
void verify_output_ownership(const crypto::public_key &ephem_pubkey, const size_t tx_output_index, const cryptonote::subaddress_index &received_subaddr, const cryptonote::account_base& account, const crypto::public_key &expected_output_pubkey);

// ----------------------------- GATHER BLOCKS ------------------------------

static std::vector<std::shared_ptr<monero_block>> get_blocks_from_txs(std::vector<std::shared_ptr<monero_tx_wallet>> txs) {
Expand Down Expand Up @@ -286,6 +312,19 @@ namespace monero_utils
return blocks;
}

// compute m_num_suggested_confirmations TODO monero-project: this logic is based on wallet_rpc_server.cpp `set_confirmations` but it should be encapsulated in wallet2
static void set_num_suggested_confirmations(std::shared_ptr<monero_incoming_transfer>& incoming_transfer, uint64_t blockchain_height, uint64_t block_reward, uint64_t unlock_time) {
if (block_reward == 0) incoming_transfer->m_num_suggested_confirmations = 0;
else incoming_transfer->m_num_suggested_confirmations = (incoming_transfer->m_amount.get() + block_reward - 1) / block_reward;

if (unlock_time < CRYPTONOTE_MAX_BLOCK_NUMBER) {
if (unlock_time > blockchain_height) incoming_transfer->m_num_suggested_confirmations = std::max(incoming_transfer->m_num_suggested_confirmations.get(), unlock_time - blockchain_height);
} else {
const uint64_t now = time(NULL);
if (unlock_time > now) incoming_transfer->m_num_suggested_confirmations = std::max(incoming_transfer->m_num_suggested_confirmations.get(), (unlock_time - now + DIFFICULTY_TARGET_V2 - 1) / DIFFICULTY_TARGET_V2);
}
}

// ------------------------------ FREE MEMORY -------------------------------

static void free(std::shared_ptr<monero_block> block) {
Expand Down
Loading
Loading