/* * This file is part of the Monero P2Pool * Copyright (c) 2021-2025 SChernykh * Portions Copyright (c) 2012-2013 The Cryptonote developers * Portions Copyright (c) 2014-2021 The Monero Project * Portions Copyright (c) 2021 XMRig * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, version 3. * * This program is distributed in the hope that it will be useful, but * WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ #include "common.h" #include "wallet.h" #include "keccak.h" #include "carrot_crypto.h" #include "crypto.h" #include extern "C" { #include "crypto-ops.h" } #include "fcmp_pp_crypto.h" LOG_CATEGORY(Wallet) namespace { // Allow only regular addresses (no integrated addresses, no subaddresses) // Values taken from cryptonote_config.h (CRYPTONOTE_PUBLIC_ADDRESS_BASE58_PREFIX) constexpr uint64_t valid_prefixes[] = { 0x180c96, 0x254c96, 0x24cc96 }; // SC1, SC1T, SC1S (mainnet, testnet, stagenet) constexpr uint64_t valid_prefixes_subaddress[] = { 0x314c96, 0x3c54c96, 0x384cc96 }; // SC1s, SC1Ts, SC1Ss constexpr std::array block_sizes{ 0, 2, 3, 5, 6, 7, 9, 10, 11 }; constexpr int num_full_blocks = p2pool::Wallet::ADDRESS_LENGTH / block_sizes.back(); constexpr int last_block_size = p2pool::Wallet::ADDRESS_LENGTH % block_sizes.back(); constexpr int block_sizes_lookup[11] = { 0, -1, 1, 2, -1, 3, 4, 5, -1, 6, 7 }; constexpr char alphabet[] = "123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz"; constexpr size_t alphabet_size = sizeof(alphabet) - 1; static_assert(alphabet_size == 58, "Check alphabet"); struct ReverseAlphabet { int8_t data[256]; int num_symbols; static constexpr ReverseAlphabet init() { ReverseAlphabet result = {}; for (int i = 0; i < 256; ++i) { result.data[i] = -1; } result.num_symbols = 0; for (size_t i = 0; i < alphabet_size; ++i) { if (result.data[static_cast(alphabet[i])] < 0) { result.data[static_cast(alphabet[i])] = static_cast(i); ++result.num_symbols; } } return result; } }; constexpr ReverseAlphabet rev_alphabet = ReverseAlphabet::init(); static_assert(rev_alphabet.num_symbols == 58, "Check alphabet"); } // namespace namespace p2pool { Wallet::Wallet(const char* address) : m_prefix(0), m_checksum(0), m_type(NetworkType::Invalid), m_subaddress(false) { if (!decode(address) && address) { LOGWARN(1, address << " failed to decode"); } } Wallet::Wallet(const Wallet& w) { operator=(w); } Wallet& Wallet::operator=(const Wallet& w) { if (this == &w) { return *this; } m_prefix = w.m_prefix; m_spendPublicKey = w.m_spendPublicKey; m_viewPublicKey = w.m_viewPublicKey; m_checksum = w.m_checksum; m_type = w.m_type; m_subaddress = w.m_subaddress; return *this; } bool Wallet::decode(const char* address) { m_type = NetworkType::Invalid; if (!address) { return false; } const size_t addr_len = strlen(address); if (addr_len < 94 || addr_len > 140) { return false; } // Calculate based on actual address length const int actual_num_full_blocks = addr_len / block_sizes.back(); const int actual_last_block_size = addr_len % block_sizes.back(); const int actual_last_block_size_index = block_sizes_lookup[actual_last_block_size]; if (actual_last_block_size_index < 0) { return false; } uint8_t data[73] = {}; int data_index = 0; const char* addr_ptr = address; // Use separate pointer for iteration for (int i = 0; i <= actual_num_full_blocks; ++i) { uint64_t num = 0; uint64_t order = 1; for (int j = ((i < actual_num_full_blocks) ? block_sizes.back() : actual_last_block_size) - 1; j >= 0; --j) { const int8_t digit = rev_alphabet.data[static_cast(addr_ptr[j])]; if (digit < 0) { return false; } uint64_t hi; const uint64_t tmp = num + umul128(order, static_cast(digit), &hi); if ((tmp < num) || hi) { return false; } num = tmp; order *= alphabet_size; } addr_ptr += (i < actual_num_full_blocks) ? block_sizes.back() : actual_last_block_size; // Advance by actual size for (int j = static_cast((i < actual_num_full_blocks) ? sizeof(num) : actual_last_block_size_index) - 1; j >= 0; --j) { data[data_index++] = static_cast(num >> (j * 8)); } } // Decode varint tag from start of data uint64_t tag = 0; int varint_len = 0; for (int i = 0; i < 8; ++i) { tag |= static_cast(data[i] & 0x7F) << (i * 7); ++varint_len; if ((data[i] & 0x80) == 0) { break; } } char hex_buf[32]; snprintf(hex_buf, sizeof(hex_buf), "0x%llx", static_cast(tag)); m_prefix = tag; switch (m_prefix) { case valid_prefixes[0]: m_type = NetworkType::Mainnet; break; case valid_prefixes[1]: m_type = NetworkType::Testnet; break; case valid_prefixes[2]: m_type = NetworkType::Stagenet; break; case valid_prefixes_subaddress[0]: m_type = NetworkType::Mainnet; m_subaddress = true; break; case valid_prefixes_subaddress[1]: m_type = NetworkType::Testnet; m_subaddress = true; break; case valid_prefixes_subaddress[2]: m_type = NetworkType::Stagenet; m_subaddress = true; break; default: return false; } memcpy(m_spendPublicKey.h, data + varint_len, HASH_SIZE); memcpy(m_viewPublicKey.h, data + varint_len + HASH_SIZE, HASH_SIZE); // DEBUG: Print what we decoded { static constexpr char log_category_prefix[] = "Wallet "; char spend_hex[65] = {0}, view_hex[65] = {0}, data_hex[200] = {0}, prefix_hex[20] = {0}; for (int i = 0; i < 32; i++) { sprintf(spend_hex + static_cast(i)*2, "%02x", m_spendPublicKey.h[i]); sprintf(view_hex + static_cast(i)*2, "%02x", m_viewPublicKey.h[i]); } for (int i = 0; i < std::min(data_index, 80); i++) { sprintf(data_hex + static_cast(i)*2, "%02x", data[i]); } sprintf(prefix_hex, "0x%llx", static_cast(m_prefix)); LOGINFO(6, "decode varint_len=" << varint_len << " data_index=" << data_index << " prefix=" << static_cast(prefix_hex)); LOGINFO(6, "decode raw_data: " << static_cast(data_hex)); LOGINFO(6, "decode spend: " << static_cast(spend_hex)); LOGINFO(6, "decode view: " << static_cast(view_hex)); } // Load checksum from correct position (at end of decoded data) memcpy(&m_checksum, data + data_index - sizeof(m_checksum), sizeof(m_checksum)); uint8_t md[200]; keccak(data, static_cast(data_index - sizeof(m_checksum)), md); uint32_t calculated_checksum; memcpy(&calculated_checksum, md, sizeof(calculated_checksum)); if (m_checksum != calculated_checksum) { LOGINFO(1, "Checksum FAILED"); m_type = NetworkType::Invalid; } if (memcmp(&m_checksum, md, sizeof(m_checksum)) != 0) { LOGINFO(1, "Checksum FAILED"); m_type = NetworkType::Invalid; } ge_p3 point; if ((ge_frombytes_vartime(&point, m_spendPublicKey.h) != 0) || (ge_frombytes_vartime(&point, m_viewPublicKey.h) != 0)) { m_type = NetworkType::Invalid; } if (!torsion_check()) { LOGWARN(1, "Torsion check failed for wallet " << *this << "! It will not be compatible with FCMP++."); } return valid(); } bool Wallet::assign(const hash& spend_pub_key, const hash& view_pub_key, NetworkType type, bool subaddress) { ge_p3 point; if ((ge_frombytes_vartime(&point, spend_pub_key.h) != 0) || (ge_frombytes_vartime(&point, view_pub_key.h) != 0)) { return false; } switch (type) { case NetworkType::Mainnet: m_prefix = subaddress ? valid_prefixes_subaddress[0] : valid_prefixes[0]; break; case NetworkType::Testnet: m_prefix = subaddress ? valid_prefixes_subaddress[1] : valid_prefixes[1]; break; case NetworkType::Stagenet: m_prefix = subaddress ? valid_prefixes_subaddress[2] : valid_prefixes[2]; break; default: m_prefix = 0; break; } m_spendPublicKey = spend_pub_key; m_viewPublicKey = view_pub_key; uint8_t data[1 + HASH_SIZE * 2]; data[0] = static_cast(m_prefix); memcpy(data + 1, spend_pub_key.h, HASH_SIZE); memcpy(data + 1 + HASH_SIZE, view_pub_key.h, HASH_SIZE); uint8_t md[200]; keccak(data, sizeof(data), md); memcpy(&m_checksum, md, sizeof(m_checksum)); m_type = type; m_subaddress = subaddress; if (!torsion_check()) { LOGWARN(1, "Torsion check failed for wallet " << *this << "! It will not be compatible with FCMP++."); // TODO: add "m_type = NetworkType::Invalid;" and return false in a later release, closer to FCMP++ hardfork } return true; } void Wallet::encode(char (&buf)[ADDRESS_LENGTH]) const { uint8_t data[73]; // Max: 8 bytes varint + 32 spend + 32 view + 4 checksum int data_index = 0; // Write prefix as varint uint64_t prefix = m_prefix; do { data[data_index++] = static_cast((prefix & 0x7F) | (prefix > 0x7F ? 0x80 : 0)); prefix >>= 7; } while (prefix); // Write public keys memcpy(data + data_index, m_spendPublicKey.h, HASH_SIZE); memcpy(data + data_index + HASH_SIZE, m_viewPublicKey.h, HASH_SIZE); // Calculate and write checksum uint8_t md[200]; const int pre_checksum_size = static_cast(data_index + HASH_SIZE * 2); keccak(data, pre_checksum_size, md); memcpy(data + pre_checksum_size, md, sizeof(m_checksum)); const int total_data_size = static_cast(pre_checksum_size + sizeof(m_checksum)); // Encode to base58 with variable-length data const int actual_num_full_blocks = static_cast(total_data_size / sizeof(uint64_t)); const int actual_last_block_bytes = static_cast(total_data_size % sizeof(uint64_t)); const int actual_last_block_size_index = actual_last_block_bytes > 0 ? block_sizes_lookup[actual_last_block_bytes] : -1; int buf_index = 0; for (int i = 0; i <= actual_num_full_blocks; ++i) { const bool is_last_block = (i == actual_num_full_blocks); const int bytes_in_block = is_last_block ? actual_last_block_bytes : static_cast(sizeof(uint64_t)); if (is_last_block && bytes_in_block == 0) break; // Read bytes in big-endian uint64_t n = 0; for (int j = 0; j < bytes_in_block; ++j) { n = (n << 8) | data[i * sizeof(uint64_t) + j]; } // Determine output block size const int output_block_size = is_last_block ? block_sizes[actual_last_block_size_index] : block_sizes.back(); // Encode to base58 for (int j = output_block_size - 1; j >= 0; --j) { const int digit = static_cast(n % alphabet_size); n /= alphabet_size; buf[buf_index + j] = alphabet[digit]; } buf_index += output_block_size; } // Null terminate buf[buf_index] = '\0'; } bool Wallet::get_eph_public_key(const hash& txkey_sec, size_t output_index, hash& eph_public_key, uint8_t& view_tag, const uint8_t* expected_view_tag) const { hash derivation; if (!generate_key_derivation(m_viewPublicKey, txkey_sec, output_index, derivation, view_tag)) { return false; } if (expected_view_tag && (view_tag != *expected_view_tag)) { return false; } if (!derive_public_key(derivation, output_index, m_spendPublicKey, eph_public_key)) { return false; } return true; } bool Wallet::torsion_check() const { ge_p3 p1, p2; if ((ge_frombytes_vartime(&p1, m_spendPublicKey.h) != 0) || (ge_frombytes_vartime(&p2, m_viewPublicKey.h) != 0)) { return false; } return !fcmp_pp::mul8_is_identity(p1) && !fcmp_pp::mul8_is_identity(p2) && fcmp_pp::torsion_check_vartime(p1) && fcmp_pp::torsion_check_vartime(p2); } bool Wallet::get_eph_public_key_carrot(const hash& tx_key_seed, uint64_t height, size_t output_index, uint64_t amount, hash& eph_public_key, uint8_t& view_tag) const { (void)output_index; // Not used - anchor derived from spend pubkey, not position // 1. Build input_context from height uint8_t input_context[33]; carrot::make_input_context_coinbase(height, input_context); // 2. Derive anchor from wallet's spend public key (position-independent) uint8_t anchor[16]; carrot::derive_deterministic_anchor_from_pubkey(tx_key_seed, m_spendPublicKey, anchor); // 3. Derive per-output ephemeral private key d_e from anchor static const uint8_t null_payment_id[8] = {0}; hash ephemeral_privkey; carrot::make_ephemeral_privkey(anchor, input_context, m_spendPublicKey, null_payment_id, ephemeral_privkey); // 4. Derive ephemeral public key D_e = d_e * B hash ephemeral_pubkey; carrot::make_ephemeral_pubkey_mainaddress(ephemeral_privkey, ephemeral_pubkey); // 5. Derive shared secret using this wallet's view pubkey hash shared_secret; if (!carrot::make_shared_secret_sender(ephemeral_privkey, m_viewPublicKey, shared_secret)) { return false; } // 6. Derive sender-receiver secret hash sender_receiver_secret; carrot::make_sender_receiver_secret(shared_secret, ephemeral_pubkey, input_context, sender_receiver_secret); // 7. Derive onetime address K_o carrot::make_onetime_address_coinbase(m_spendPublicKey, sender_receiver_secret, amount, eph_public_key); // 8. Derive view tag uint8_t view_tag_full[3]; carrot::make_view_tag(shared_secret, input_context, eph_public_key, view_tag_full); view_tag = view_tag_full[0]; return true; } } // namespace p2pool