From cb9fc89b22390769706d8f5e76ad5018fcc4b9fe Mon Sep 17 00:00:00 2001 From: SChernykh <15806605+SChernykh@users.noreply.github.com> Date: Mon, 20 Oct 2025 13:06:45 +0200 Subject: [PATCH] Added `--onion-address` command line option to support incoming TOR connections --- docs/COMMAND_LINE.MD | 1 + src/block_template.cpp | 24 +++--- src/block_template.h | 3 +- src/keccak.h | 4 +- src/keccak_constexpr.h | 51 +++--------- src/log.h | 5 ++ src/main.cpp | 1 + src/merge_mining_client_tari.cpp | 33 ++++---- src/p2p_server.cpp | 73 +++++++++++++--- src/p2p_server.h | 4 + src/p2pool.cpp | 2 +- src/params.cpp | 14 ++++ src/params.h | 7 ++ src/side_chain.cpp | 58 +++++++++---- src/side_chain.h | 10 ++- src/util.cpp | 121 ++++++++++++++++++++++++++- src/util.h | 3 + tests/src/block_template_tests.cpp | 25 +++--- tests/src/keccak_tests.cpp | 38 +++++++++ tests/src/pool_block_tests.cpp | 13 +-- tests/src/util_tests.cpp | 128 +++++++++++++++++++++++++++++ 21 files changed, 493 insertions(+), 125 deletions(-) diff --git a/docs/COMMAND_LINE.MD b/docs/COMMAND_LINE.MD index e469677..4b45811 100644 --- a/docs/COMMAND_LINE.MD +++ b/docs/COMMAND_LINE.MD @@ -41,6 +41,7 @@ --rpc-ssl-fingerprint base64-encoded fingerprint of the Monero node's certificate (optional, use it for certificate pinning) --no-stratum-http Disable HTTP on Stratum ports --full-validation Enables full share validation / increases CPU usage +--onion-address Tell other peers to use this .onion address to connect to this node through TOR ``` ### Example command line diff --git a/src/block_template.cpp b/src/block_template.cpp index a5d2542..88fdeed 100644 --- a/src/block_template.cpp +++ b/src/block_template.cpp @@ -204,7 +204,7 @@ void BlockTemplate::shuffle_tx_order() } } -void BlockTemplate::update(const MinerData& data, const Mempool& mempool, const Wallet& miner_wallet, const Wallet& subaddress) +void BlockTemplate::update(const MinerData& data, const Mempool& mempool, const Params* params) { if (data.major_version > HARDFORK_SUPPORTED_VERSION) { LOGERR(1, "got hardfork version " << data.major_version << ", expected <= " << HARDFORK_SUPPORTED_VERSION); @@ -269,7 +269,7 @@ void BlockTemplate::update(const MinerData& data, const Mempool& mempool, const m_blockHeaderSize = m_blockHeader.size(); - m_poolBlockTemplate->m_minerWallet = miner_wallet; + m_poolBlockTemplate->m_minerWallet = params->m_miningWallet; if (!m_sidechain->fill_sidechain_data(*m_poolBlockTemplate, m_shares)) { use_old_template(); @@ -595,7 +595,7 @@ void BlockTemplate::update(const MinerData& data, const Mempool& mempool, const m_poolBlockTemplate->m_transactions.push_back(m_mempoolTxs[m_mempoolTxsOrder[i]].id); } - m_poolBlockTemplate->m_minerWallet = miner_wallet; + m_poolBlockTemplate->m_minerWallet = params->m_miningWallet; // Layout: [software id, version, random number, sidechain extra_nonce] uint32_t* sidechain_extra = m_poolBlockTemplate->m_sidechainExtraBuf; @@ -630,16 +630,18 @@ void BlockTemplate::update(const MinerData& data, const Mempool& mempool, const m_poolBlockTemplate->m_mergeMiningExtra.emplace(c.unique_id, std::move(v)); } - if (subaddress.valid()) { - std::vector v; - v.reserve(HASH_SIZE + 2); + if (params->m_subaddress.valid()) { + uint8_t buf[HASH_SIZE + 2] = {}; + memcpy(buf, params->m_subaddress.view_public_key().h, HASH_SIZE); - const hash& key = subaddress.view_public_key(); - v.insert(v.end(), key.h, key.h + HASH_SIZE); - v.push_back(0); - v.push_back(0); + m_poolBlockTemplate->m_mergeMiningExtra.emplace(keccak_subaddress_viewpub, std::vector(buf, buf + sizeof(buf))); + } - m_poolBlockTemplate->m_mergeMiningExtra.emplace(keccak_subaddress_viewpub, std::move(v)); + if (!params->m_onionPubkey.empty()) { + uint8_t buf[HASH_SIZE + 2] = {}; + memcpy(buf, params->m_onionPubkey.h, HASH_SIZE); + + m_poolBlockTemplate->m_mergeMiningExtra.emplace(keccak_onion_address_v3, std::vector(buf, buf + sizeof(buf))); } init_merge_mining_merkle_proof(); diff --git a/src/block_template.h b/src/block_template.h index b6a256f..11fbdae 100644 --- a/src/block_template.h +++ b/src/block_template.h @@ -29,6 +29,7 @@ class Mempool; class Wallet; struct PoolBlock; struct MinerShare; +struct Params; class BlockTemplate { @@ -39,7 +40,7 @@ public: BlockTemplate(const BlockTemplate& b); BlockTemplate& operator=(const BlockTemplate& b); - void update(const MinerData& data, const Mempool& mempool, const Wallet& miner_wallet, const Wallet& subaddress); + void update(const MinerData& data, const Mempool& mempool, const Params* params); uint64_t last_updated() const { return m_lastUpdated.load(); } bool get_difficulties(const uint32_t template_id, uint64_t& height, uint64_t& sidechain_height, difficulty_type& mainchain_difficulty, difficulty_type& aux_diff, difficulty_type& sidechain_difficulty) const; diff --git a/src/keccak.h b/src/keccak.h index 8d73419..a010d6a 100644 --- a/src/keccak.h +++ b/src/keccak.h @@ -49,7 +49,7 @@ FORCEINLINE void keccak(const uint8_t* in, int inlen, uint8_t (&md)[N]) } template -FORCEINLINE void keccak_custom(T&& in, int inlen, uint8_t* md, int mdlen) +FORCEINLINE void keccak_custom(T&& in, int inlen, uint8_t* md, int mdlen, bool SHA3 = false) { std::array st = {}; @@ -76,7 +76,7 @@ FORCEINLINE void keccak_custom(T&& in, int inlen, uint8_t* md, int mdlen) temp[i] = in(offset + i); } - temp[inlen++] = 1; + temp[inlen++] = SHA3 ? 6 : 1; memset(temp + inlen, 0, rsiz - inlen); temp[rsiz - 1] |= 0x80; diff --git a/src/keccak_constexpr.h b/src/keccak_constexpr.h index 20989fa..158304f 100644 --- a/src/keccak_constexpr.h +++ b/src/keccak_constexpr.h @@ -22,9 +22,6 @@ namespace p2pool { namespace ConstexprKeccak { -template -static FORCEINLINE constexpr uint64_t rotl64(uint64_t x) { return (x << y) | (x >> (64 - y)); } - template static FORCEINLINE constexpr void keccakf(std::array& st) { @@ -39,6 +36,8 @@ static FORCEINLINE constexpr void keccakf(std::array& st) 0x000000000000800a, 0x800000008000000a, 0x8000000080008081, 0x8000000000008080, 0x0000000080000001, 0x8000000080008008 }; + constexpr int order[25] = { 1, 6, 9, 22, 14, 20, 2, 12, 13, 19, 23, 15, 4, 24, 21, 8, 16, 5, 3, 18, 17, 11, 7, 10, 1 }; + constexpr int shift[24] = { 44, 20, 61, 39, 18, 62, 43, 25, 8, 56, 41, 27, 14, 2, 55, 45, 36, 28, 21, 15, 10, 6, 3, 1 }; for (int round = 0; round < ROUNDS; ++round) { uint64_t bc[5] = {}; @@ -47,36 +46,13 @@ static FORCEINLINE constexpr void keccakf(std::array& st) for (int i = 0; i < 5; ++i) bc[i] = st[i] ^ st[i + 5] ^ st[i + 10] ^ st[i + 15] ^ st[i + 20]; for (int i = 0; i < 5; ++i) { - const uint64_t t = bc[(i + 4) % 5] ^ rotl64<1>(bc[(i + 1) % 5]); + const uint64_t t = bc[(i + 4) % 5] ^ ((bc[(i + 1) % 5] << 1) | (bc[(i + 1) % 5] >> 63)); for (int j = 0; j < 25; j += 5) st[i + j] ^= t; } // Rho Pi - const uint64_t st1 = st[1]; - st[ 1] = rotl64<44>(st[ 6]); - st[ 6] = rotl64<20>(st[ 9]); - st[ 9] = rotl64<61>(st[22]); - st[22] = rotl64<39>(st[14]); - st[14] = rotl64<18>(st[20]); - st[20] = rotl64<62>(st[ 2]); - st[ 2] = rotl64<43>(st[12]); - st[12] = rotl64<25>(st[13]); - st[13] = rotl64< 8>(st[19]); - st[19] = rotl64<56>(st[23]); - st[23] = rotl64<41>(st[15]); - st[15] = rotl64<27>(st[ 4]); - st[ 4] = rotl64<14>(st[24]); - st[24] = rotl64< 2>(st[21]); - st[21] = rotl64<55>(st[ 8]); - st[ 8] = rotl64<45>(st[16]); - st[16] = rotl64<36>(st[ 5]); - st[ 5] = rotl64<28>(st[ 3]); - st[ 3] = rotl64<21>(st[18]); - st[18] = rotl64<15>(st[17]); - st[17] = rotl64<10>(st[11]); - st[11] = rotl64< 6>(st[ 7]); - st[ 7] = rotl64< 3>(st[10]); - st[10] = rotl64< 1>(st1); + const auto st0 = st; + for (int i = 0; i < 24; ++i) st[order[i]] = (st0[order[i + 1]] << shift[i]) | (st0[order[i + 1]] >> (64 - shift[i])); // Chi for (int i = 0; i < 25; i += 5) { @@ -94,20 +70,11 @@ static FORCEINLINE constexpr void keccakf(std::array& st) template static constexpr hash keccak(const char (&input)[len]) { - hash result{}; - - if constexpr (len <= 0) { - return result; - } - - constexpr int inlen = len - 1; - constexpr int rsiz = 136; - constexpr int rsizw = rsiz / 8; - + constexpr int inlen = len - 1; static_assert(inlen < rsiz, "Too long input"); - uint8_t temp[144] = {}; + uint8_t temp[rsiz] = {}; for (int i = 0; i < inlen; ++i) { temp[i] = static_cast(input[i]); @@ -118,7 +85,7 @@ static constexpr hash keccak(const char (&input)[len]) std::array st = {}; - for (int i = 0; i < rsizw; i++) { + for (int i = 0; i < rsiz / 8; i++) { uint64_t k = 0; for (int j = 0; j < 8; ++j) { k |= static_cast(temp[i * 8 + j]) << (j * 8); @@ -128,6 +95,8 @@ static constexpr hash keccak(const char (&input)[len]) ConstexprKeccak::keccakf<24>(st); + hash result{}; + for (size_t i = 0; i < HASH_SIZE; ++i) { result.h[i] = static_cast(st[i / 8] >> ((i % 8) * 8)); } diff --git a/src/log.h b/src/log.h index 9cf1196..469ccac 100644 --- a/src/log.h +++ b/src/log.h @@ -184,6 +184,11 @@ template<> struct Stream::Entry static FORCEINLINE void put(char c, Stream* wrapper) { wrapper->writeBuf(&c, 1); } }; +template<> struct Stream::Entry +{ + static FORCEINLINE void put(const std::string_view& data, Stream* wrapper) { wrapper->writeBuf(data.data(), data.size()); } +}; + #define INT_ENTRY(x) \ template<> struct Stream::Entry { static FORCEINLINE void put(x data, Stream* wrapper) { wrapper->writeInt(data); } }; diff --git a/src/main.cpp b/src/main.cpp index aa8a29d..710cb67 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -106,6 +106,7 @@ void p2pool_usage() #endif "--no-stratum-http Disable HTTP on Stratum ports\n" "--full-validation Enables full share validation / increases CPU usage\n" + "--onion-address Tell other peers to use this .onion address to connect to this node through TOR\n" "--help Show this help message\n\n" "Example command line:\n\n" "%s --host 127.0.0.1 --rpc-port 18081 --zmq-port 18083 --wallet YOUR_WALLET_ADDRESS --stratum 0.0.0.0:%d --p2p 0.0.0.0:%d\n\n", diff --git a/src/merge_mining_client_tari.cpp b/src/merge_mining_client_tari.cpp index 4ac0b7b..cab0858 100644 --- a/src/merge_mining_client_tari.cpp +++ b/src/merge_mining_client_tari.cpp @@ -235,27 +235,24 @@ void MergeMiningClientTari::on_external_block(const PoolBlock& block) return; } - std::vector>> mm_extra; - mm_extra.reserve(block.m_mergeMiningExtra.size()); - - // Filter aux chain data only - for (const auto& i : block.m_mergeMiningExtra) { - if ((i.first != keccak_subaddress_viewpub) && - (i.first != keccak_onion_address_v3)) { - mm_extra.emplace_back(i.first, i.second); - } - } - - std::vector aux_ids; - std::vector aux_chains; - // All aux chains in this block + the P2Pool sidechain - aux_ids.reserve(mm_extra.size() + 1); + std::vector aux_ids; // All aux chains in this block - aux_chains.reserve(mm_extra.size()); + std::vector aux_chains; + + aux_ids.reserve(block.m_mergeMiningExtra.size() + 1); + aux_chains.reserve(block.m_mergeMiningExtra.size() + 1); + + uint64_t mm_extra_size = 0; + + for (const auto& i : block.m_mergeMiningExtra) { + // Filter aux chain data only + if ((i.first == keccak_subaddress_viewpub) || (i.first == keccak_onion_address_v3)) { + continue; + } + ++mm_extra_size; - for (const auto& i : mm_extra) { hash data; difficulty_type diff; { @@ -383,7 +380,7 @@ void MergeMiningClientTari::on_external_block(const PoolBlock& block) uint32_t aux_merkle_proof_path = 0; const hash sidechain_id = block.m_sidechainId; - const uint32_t n_aux_chains = static_cast(mm_extra.size() + 1); + const uint32_t n_aux_chains = static_cast(mm_extra_size + 1); std::vector hashes(n_aux_chains); diff --git a/src/p2p_server.cpp b/src/p2p_server.cpp index 1eb033d..85f20f9 100644 --- a/src/p2p_server.cpp +++ b/src/p2p_server.cpp @@ -72,6 +72,7 @@ P2PServer::P2PServer(p2pool* pool) , m_timerInterval(2) , m_seenGoodPeers(false) , m_peerListLastSaved(0) + , m_numOnionConnections(0) , m_lookForMissingBlocks(true) , m_fastestPeer(nullptr) , m_newP2PoolVersionDetected(false) @@ -318,8 +319,14 @@ void P2PServer::update_peer_connections() uint64_t newer_version_p2pool = 0; unordered_set connected_clients; + unordered_set connected_clients_domain; connected_clients.reserve(m_numConnections); + + if (!m_socks5Proxy.empty()) { + connected_clients_domain.reserve(m_numConnections); + } + for (P2PClient* client = static_cast(m_connectedClientsList->m_next); client != m_connectedClientsList; client = static_cast(client->m_next)) { const int timeout = client->m_handshakeComplete ? 300 : 10; if (client->m_lastAlive && (cur_time >= client->m_lastAlive + timeout)) { @@ -346,7 +353,19 @@ void P2PServer::update_peer_connections() } } - connected_clients.insert(client->m_addr); + if ((client->m_addressType == Client::AddressType::IPv4) || (client->m_addressType == Client::AddressType::IPv6)) { + connected_clients.insert(client->m_addr); + } + else if (!m_socks5Proxy.empty() && (client->m_addressType == Client::AddressType::DomainName)) { + const char* c = strchr(client->m_addrString, ':'); + if (c) { + connected_clients_domain.emplace(client->m_addrString, c - client->m_addrString); + } + else { + connected_clients_domain.emplace(client->m_addrString); + } + } + if (client->is_good()) { has_good_peers = true; if ((client->m_pingTime >= 0) && (!m_fastestPeer || (m_fastestPeer->m_pingTime > client->m_pingTime))) { @@ -397,18 +416,36 @@ void P2PServer::update_peer_connections() N = static_cast(peer_list.size()); } + uint32_t num_outgoing = m_numConnections - m_numIncomingConnections; + + // Try to have at least N/2 outgoing onion connections + if (!m_socks5Proxy.empty()) { + std::vector pubkeys = m_pool->side_chain().seen_onion_pubkeys(); + + for (uint32_t i = m_numOnionConnections, n = N / 2; (i < n) && !pubkeys.empty();) { + const uint64_t k = get_random64() % pubkeys.size(); + const std::string addr = to_onion_v3(pubkeys[k]); + + if ((connected_clients_domain.find(addr) == connected_clients_domain.end()) && connect_to_peer(addr, DEFAULT_P2P_PORT_ONION)) { + ++i; + ++num_outgoing; + } + + pubkeys[k] = pubkeys.back(); + pubkeys.pop_back(); + } + } + // Try to have at least N outgoing connections (N defaults to 10, can be set via --out-peers command line parameter) - for (uint32_t i = m_numConnections - m_numIncomingConnections; (i < N) && !peer_list.empty();) { + while ((num_outgoing < N) && !peer_list.empty()) { const uint64_t k = get_random64() % peer_list.size(); const Peer& peer = peer_list[k]; if ((connected_clients.find(peer.m_addr) == connected_clients.end()) && connect_to_peer(peer.m_isV6, peer.m_addr, peer.m_port)) { - ++i; + ++num_outgoing; } - if (k + 1 < peer_list.size()) { - peer_list[k] = peer_list.back(); - } + peer_list[k] = peer_list.back(); peer_list.pop_back(); } @@ -1091,12 +1128,15 @@ uint64_t P2PServer::get_random64() void P2PServer::print_status() { + const uint64_t onion_list_size = m_pool->side_chain().onion_pubkeys_count(); + MutexLock lock(m_peerListLock); LOGINFO(0, "status" << - "\nConnections = " << m_numConnections.load() << " (" << m_numIncomingConnections.load() << " incoming)" << - "\nPeer list size = " << m_peerList.size() << - "\nUptime = " << log::Duration(seconds_since_epoch() - m_pool->start_time()) + "\nConnections = " << m_numConnections.load() << " (" << m_numIncomingConnections.load() << " incoming, " << m_numOnionConnections.load() << " onion)" + "\nPeer list size = " << m_peerList.size() << + "\nOnion list size = " << onion_list_size << + "\nUptime = " << log::Duration(seconds_since_epoch() - m_pool->start_time()) ); } @@ -1780,8 +1820,13 @@ void P2PServer::P2PClient::reset() { P2PServer* server = static_cast(m_owner); - if (server && (server->m_fastestPeer == this)) { - server->m_fastestPeer = nullptr; + if (server) { + if (server->m_fastestPeer == this) { + server->m_fastestPeer = nullptr; + } + if (m_addressType == AddressType::DomainName) { + --server->m_numOnionConnections; + } } Client::reset(); @@ -1818,7 +1863,7 @@ void P2PServer::P2PClient::reset() bool P2PServer::P2PClient::on_connect() { - const P2PServer* server = static_cast(m_owner); + P2PServer* server = static_cast(m_owner); if (!server) { return false; @@ -1839,6 +1884,10 @@ bool P2PServer::P2PClient::on_connect() } } + if (m_addressType == AddressType::DomainName) { + ++server->m_numOnionConnections; + } + const uint64_t cur_time = seconds_since_epoch(); m_connectedTime = cur_time; m_lastAlive = cur_time; diff --git a/src/p2p_server.h b/src/p2p_server.h index 05db10e..e424ad0 100644 --- a/src/p2p_server.h +++ b/src/p2p_server.h @@ -36,6 +36,8 @@ static constexpr int DEFAULT_P2P_PORT = 37889; static constexpr int DEFAULT_P2P_PORT_MINI = 37888; static constexpr int DEFAULT_P2P_PORT_NANO = 37890; +static constexpr int DEFAULT_P2P_PORT_ONION = 28722; + static constexpr uint32_t PROTOCOL_VERSION_1_0 = 0x00010000UL; static constexpr uint32_t PROTOCOL_VERSION_1_1 = 0x00010001UL; static constexpr uint32_t PROTOCOL_VERSION_1_2 = 0x00010002UL; @@ -288,6 +290,8 @@ private: std::vector m_peerListMonero; std::atomic m_peerListLastSaved; + std::atomic m_numOnionConnections; + uv_mutex_t m_broadcastLock; uv_async_t m_broadcastAsync; std::vector m_broadcastQueue; diff --git a/src/p2pool.cpp b/src/p2pool.cpp index 6782265..fd065b4 100644 --- a/src/p2pool.cpp +++ b/src/p2pool.cpp @@ -1246,7 +1246,7 @@ void p2pool::update_block_template() if (m_updateSeed.exchange(false)) { m_hasher->set_seed_async(data.seed_hash); } - m_blockTemplate->update(data, *m_mempool, m_params->m_miningWallet, m_params->m_subaddress); + m_blockTemplate->update(data, *m_mempool, m_params); stratum_on_block(); api_update_pool_stats(); diff --git a/src/params.cpp b/src/params.cpp index d24f928..f0aa224 100644 --- a/src/params.cpp +++ b/src/params.cpp @@ -268,6 +268,11 @@ Params::Params(int argc, char* const argv[]) ok = true; } + if ((strcmp(argv[i], "--onion-address") == 0) && (i + 1 < argc)) { + m_onionAddress = argv[++i]; + ok = true; + } + if (!ok) { // Wait to avoid log messages overlapping with printf() calls and making a mess on screen std::this_thread::sleep_for(std::chrono::milliseconds(10)); @@ -278,6 +283,15 @@ Params::Params(int argc, char* const argv[]) } } + if (!m_onionAddress.empty()) { + m_onionPubkey = from_onion_v3(m_onionAddress); + + if (m_onionPubkey.empty()) { + LOGERR(1, "Failed to parse \"" << m_onionAddress << '"'); + throw std::exception(); + } + } + auto invalid_host = [](const Host& h) { if (!h.valid()) { diff --git a/src/params.h b/src/params.h index 5e3087e..c12e22d 100644 --- a/src/params.h +++ b/src/params.h @@ -25,6 +25,10 @@ static constexpr uint64_t DEFAULT_STRATUM_BAN_TIME = 600; struct Params { +#ifdef P2POOL_UNIT_TESTS + FORCEINLINE Params() {} +#endif + Params(int argc, char* const argv[]); bool valid() const; @@ -124,6 +128,9 @@ struct Params }; #endif bool m_enableFullValidation = false; + + std::string m_onionAddress; + hash m_onionPubkey; }; } // namespace p2pool diff --git a/src/side_chain.cpp b/src/side_chain.cpp index dc2c810..6b5611b 100644 --- a/src/side_chain.cpp +++ b/src/side_chain.cpp @@ -101,7 +101,6 @@ SideChain::SideChain(p2pool* pool, NetworkType type, const char* pool_name) m_curDifficulty = m_minDifficulty; uv_rwlock_init_checked(&m_sidechainLock); - uv_mutex_init_checked(&m_seenWalletsLock); uv_mutex_init_checked(&m_incomingBlocksLock); uv_rwlock_init_checked(&m_curDifficultyLock); uv_rwlock_init_checked(&m_watchBlockLock); @@ -226,7 +225,6 @@ SideChain::~SideChain() finish_precalc(); uv_rwlock_destroy(&m_sidechainLock); - uv_mutex_destroy(&m_seenWalletsLock); uv_mutex_destroy(&m_incomingBlocksLock); uv_rwlock_destroy(&m_curDifficultyLock); uv_rwlock_destroy(&m_watchBlockLock); @@ -693,8 +691,18 @@ bool SideChain::add_block(const PoolBlock& block) PoolBlock* new_block = new PoolBlock(block); { - MutexLock lock(m_seenWalletsLock); + WriteLock lock(m_seenDataLock); + m_seenWallets[new_block->m_minerWallet.spend_public_key()] = new_block->m_localTimestamp; + + auto it = new_block->m_mergeMiningExtra.find(keccak_onion_address_v3); + if ((it != new_block->m_mergeMiningExtra.end()) && (it->second.size() >= HASH_SIZE)) { + hash h; + memcpy(h.h, it->second.data(), HASH_SIZE); + m_seenOnionPubkeys[h] = new_block->m_localTimestamp; + } + + prune_seen_data(); } WriteLock lock(m_sidechainLock); @@ -1143,26 +1151,29 @@ difficulty_type SideChain::total_hashes() const return tip ? tip->m_cumulativeDifficulty : difficulty_type(); } -uint64_t SideChain::miner_count() +// Expects that m_seenDataLock is already locked for writing +void SideChain::prune_seen_data() { const uint64_t cur_time = seconds_since_epoch(); - MutexLock lock(m_seenWalletsLock); - - // Every 5 minutes, delete wallets that weren't seen for more than 72 hours + // Every 5 minutes, delete wallets that weren't seen for more than 72 hours and onion pubkeys that weren't seen for more than 12 hours if (m_seenWalletsLastPruneTime + 5ul * 60ul <= cur_time) { - for (auto it = m_seenWallets.begin(); it != m_seenWallets.end();) { - if (it->second + 72ul * 60ul * 60ul < cur_time) { - it = m_seenWallets.erase(it); + auto prune = [cur_time](auto& data, uint64_t timeout) { + for (auto it = data.begin(); it != data.end();) { + if (it->second + timeout < cur_time) { + it = data.erase(it); + } + else { + ++it; + } } - else { - ++it; - } - } + }; + + prune(m_seenWallets, 72 * 3600); + prune(m_seenOnionPubkeys, 12 * 3600); + m_seenWalletsLastPruneTime = cur_time; } - - return m_seenWallets.size(); } uint64_t SideChain::last_updated() const @@ -1356,6 +1367,21 @@ bool SideChain::p2pool_update_available() const return newer_p2pool_diff * 5 >= total_p2pool_diff; } +std::vector SideChain::seen_onion_pubkeys() const +{ + std::vector result; + + ReadLock lock(m_seenDataLock); + + result.reserve(m_seenOnionPubkeys.size()); + + for (const auto& it : m_seenOnionPubkeys) { + result.push_back(it.first); + } + + return result; +} + void SideChain::verify_loop(PoolBlock* block) { // PoW is already checked at this point diff --git a/src/side_chain.h b/src/side_chain.h index 6c36c47..b9c0481 100644 --- a/src/side_chain.h +++ b/src/side_chain.h @@ -74,7 +74,8 @@ public: [[nodiscard]] FORCEINLINE difficulty_type difficulty() const { ReadLock lock(m_curDifficultyLock); return m_curDifficulty; } [[nodiscard]] difficulty_type total_hashes() const; [[nodiscard]] uint64_t block_time() const { return m_targetBlockTime; } - [[nodiscard]] uint64_t miner_count(); + [[nodiscard]] FORCEINLINE uint64_t miner_count() const { ReadLock lock(m_seenDataLock); return m_seenWallets.size(); } + [[nodiscard]] FORCEINLINE uint64_t onion_pubkeys_count() const { ReadLock lock(m_seenDataLock); return m_seenOnionPubkeys.size(); } [[nodiscard]] uint64_t last_updated() const; [[nodiscard]] bool is_default() const; [[nodiscard]] bool is_mini() const; @@ -86,6 +87,8 @@ public: [[nodiscard]] bool p2pool_update_available() const; + [[nodiscard]] std::vector seen_onion_pubkeys() const; + #ifdef P2POOL_UNIT_TESTS difficulty_type m_testMainChainDiff; const unordered_map& blocksById() const { return m_blocksById; } @@ -114,14 +117,17 @@ private: [[nodiscard]] bool load_config(const std::string& filename); [[nodiscard]] bool check_config() const; + void prune_seen_data(); + mutable uv_rwlock_t m_sidechainLock; std::atomic m_chainTip; std::map> m_blocksByHeight; unordered_map m_blocksById; unordered_map m_blocksByMerkleRoot; - uv_mutex_t m_seenWalletsLock; + mutable ReadWriteLock m_seenDataLock; unordered_map m_seenWallets; + unordered_map m_seenOnionPubkeys; uint64_t m_seenWalletsLastPruneTime; // Used to quickly cut off multiple broadcasts of the same block by different peers. Only the first broadcast will be processed. diff --git a/src/util.cpp b/src/util.cpp index 11dc39f..30e9fb3 100644 --- a/src/util.cpp +++ b/src/util.cpp @@ -18,6 +18,12 @@ #include "common.h" #include "util.h" #include "uv_util.h" +#include "keccak.h" + +extern "C" { +#include "crypto-ops.h" +} + #include #include #include @@ -480,7 +486,7 @@ struct BackgroundJobTracker::Impl result.reserve(m_jobs.size()); for (const auto& job : m_jobs) { - result.emplace_back(job.first, job.second); + result.emplace_back(job.first.data(), job.second); } } return result; @@ -505,10 +511,8 @@ struct BackgroundJobTracker::Impl LOGINFO(0, "background jobs running:" << log::const_buf(buf, s.m_pos)); } - struct Compare { FORCEINLINE bool operator()(const char* a, const char* b) const { return strcmp(a, b) < 0; } }; - uv_mutex_t m_lock; - std::map m_jobs; + std::map m_jobs; }; BackgroundJobTracker::BackgroundJobTracker() : m_impl(new Impl()) @@ -947,4 +951,113 @@ void set_thread_name(const char* name) #endif } +std::string to_onion_v3(const hash& pubkey) +{ + static constexpr uint8_t prefix[] = ".onion checksum"; + static constexpr uint8_t version = 3; + + hash h; + keccak_custom([&pubkey](int offset) { + size_t k = static_cast(offset); + if (k < sizeof(prefix) - 1) { + return prefix[k]; + } + k -= sizeof(prefix) - 1; + + return (k < HASH_SIZE) ? pubkey.h[k] : version; + }, sizeof(prefix) + HASH_SIZE, h.h, HASH_SIZE, true); + + // pubkey (32 bytes), checksum (2 bytes), version (1 byte), and a zero byte for padding + uint8_t buf[HASH_SIZE + 4]; + + memcpy(buf, pubkey.h, HASH_SIZE); + memcpy(buf + HASH_SIZE, h.h, 2); + buf[HASH_SIZE + 2] = version; + buf[HASH_SIZE + 3] = 0; + + std::string result; + result.reserve(62); + + uint64_t data = 0; + uint64_t bit_size = 0; + + for (size_t i = 0; i < HASH_SIZE + 3; ++i) { + data = (data << 8) | buf[i]; + bit_size += 8; + + while (bit_size >= 5) { + bit_size -= 5; + result += "abcdefghijklmnopqrstuvwxyz234567"[(data >> bit_size) & 31]; + } + } + + result.append(".onion"); + + return result; +} + +hash from_onion_v3(const std::string& address) +{ + if ((address.length() < 6) || (address.find(".onion") != address.length() - 6)) { + LOGWARN(3, "Invalid onion address \"" << address << "\": doesn't end with \".onion\""); + return {}; + } + + if (address.length() != 62) { + LOGWARN(3, "Invalid onion address \"" << address << "\": expected length 62, got " << address.length() ); + return {}; + } + + uint8_t buf[HASH_SIZE + 4] = {}; + uint8_t* p = buf; + + uint64_t data = 0; + uint64_t bit_size = 0; + + for (size_t i = 0; i < 56; ++i) { + const char c = address[i]; + uint64_t digit; + + if ('a' <= c && c <= 'z') { + digit = static_cast(c - 'a'); + } + else if ('A' <= c && c <= 'Z') { + digit = static_cast(c - 'A'); + } + else if ('2' <= c && c <= '7') { + digit = static_cast(c - '2') + 26; + } + else { + LOGWARN(3, "Invalid onion address \"" << address << "\": has an invalid character \"" << c << '"'); + return {}; + } + + data = (data << 5) | digit; + bit_size += 5; + + while (bit_size >= 8) { + bit_size -= 8; + *(p++) = static_cast(data >> bit_size); + } + } + + hash result; + memcpy(result.h, buf, HASH_SIZE); + + // Checksum validation + if (to_onion_v3(result) != address) { + LOGWARN(3, "Invalid onion address \"" << address << "\": checksum failed"); + return {}; + } + + // Pubkey validation + ge_p3 point; + if (ge_frombytes_vartime(&point, result.h) != 0) { + LOGWARN(3, "Invalid onion address \"" << address << "\": invalid ed25519 pubkey"); + return {}; + } + + return result; +} + } // namespace p2pool diff --git a/src/util.h b/src/util.h index e40acf6..ae2d6dd 100644 --- a/src/util.h +++ b/src/util.h @@ -397,6 +397,9 @@ FORCEINLINE void secure_zero_memory(T& value) secure_zero_memory(&value, sizeof(T)); } +std::string to_onion_v3(const hash& pubkey); +hash from_onion_v3(const std::string& address); + } // namespace p2pool void memory_tracking_start(); diff --git a/tests/src/block_template_tests.cpp b/tests/src/block_template_tests.cpp index 5cfe1f7..982cd98 100644 --- a/tests/src/block_template_tests.cpp +++ b/tests/src/block_template_tests.cpp @@ -22,6 +22,7 @@ #include "side_chain.h" #include "wallet.h" #include "keccak.h" +#include "params.h" #include "gtest/gtest.h" namespace p2pool { @@ -52,11 +53,12 @@ TEST(block_template, update) data.median_timestamp = (1ULL << 35) - 2; Mempool mempool; - Wallet wallet("44MnN1f3Eto8DZYUWuE5XZNUtE3vcRzt2j6PzqWpPau34e6Cf4fAxt6X2MBmrm6F9YMEiMNjN6W4Shn4pLcfNAja621jwyg"); - Wallet subaddress{ nullptr }; + Params params; + + params.m_miningWallet = Wallet("44MnN1f3Eto8DZYUWuE5XZNUtE3vcRzt2j6PzqWpPau34e6Cf4fAxt6X2MBmrm6F9YMEiMNjN6W4Shn4pLcfNAja621jwyg"); // Test 1: empty template - tpl.update(data, mempool, wallet, subaddress); + tpl.update(data, mempool, ¶ms); ASSERT_EQ(tpl.get_reward(), 600000000000ULL); const PoolBlock* b = tpl.pool_block_template(); @@ -111,7 +113,7 @@ TEST(block_template, update) } ASSERT_EQ(mempool.size(), 512); - tpl.update(data, mempool, wallet, subaddress); + tpl.update(data, mempool, ¶ms); ASSERT_EQ(tpl.get_reward(), 612054770773ULL); ASSERT_EQ(b->m_sidechainId, H("c9df4853003ab436416b9fc9a5a072d16b4dede849e697a8be2ebb9c88c8ec72")); @@ -152,7 +154,7 @@ TEST(block_template, update) data.aux_chains.emplace_back(H("01f0cf665bd4cd31cbb2b2470236389c483522b350335e10a4a5dca34cb85990"), H("d9de1cfba7cdbd47f12f77addcb39b24c1ae7a16c35372bf28d6aee5d7579ee6"), difficulty_type(1000000)); - tpl.update(data, mempool, wallet, subaddress); + tpl.update(data, mempool, ¶ms); ASSERT_EQ(tpl.get_reward(), 600300000000ULL); ASSERT_EQ(b->m_sidechainId, H("c32abac2cad40e263a94f5f43f90e0a7d7d4b151305b79951dbc8c88c3180613")); @@ -189,7 +191,7 @@ TEST(block_template, update) } ASSERT_EQ(mempool.size(), 10000); - tpl.update(data, mempool, wallet, subaddress); + tpl.update(data, mempool, ¶ms); ASSERT_EQ(tpl.get_reward(), 619742028747ULL); ASSERT_EQ(b->m_sidechainId, H("69e7dd43dd99ac6be3f57ca333cc0d814189e83aee1773c99a341aca085c0d46")); @@ -242,19 +244,20 @@ TEST(block_template, submit_sidechain_block) data.median_timestamp = (1ULL << 35) - (sidechain.chain_window_size() * 2 + 10) * sidechain.block_time() - 3600; Mempool mempool; - Wallet wallet("44MnN1f3Eto8DZYUWuE5XZNUtE3vcRzt2j6PzqWpPau34e6Cf4fAxt6X2MBmrm6F9YMEiMNjN6W4Shn4pLcfNAja621jwyg"); - Wallet subaddress{ nullptr }; + Params params; + + params.m_miningWallet = Wallet("44MnN1f3Eto8DZYUWuE5XZNUtE3vcRzt2j6PzqWpPau34e6Cf4fAxt6X2MBmrm6F9YMEiMNjN6W4Shn4pLcfNAja621jwyg"); std::mt19937_64 rng(101112); for (uint64_t i = 0, i2 = 0, i3 = 0; i < sidechain.chain_window_size() * 3; ++i) { - tpl.update(data, mempool, wallet, subaddress); + tpl.update(data, mempool, ¶ms); if ((rng() % 31) == 0) { - tpl2.update(data, mempool, wallet, subaddress); + tpl2.update(data, mempool, ¶ms); if ((rng() % 11) == 0) { - tpl3.update(data, mempool, wallet, subaddress); + tpl3.update(data, mempool, ¶ms); ++i3; ASSERT_TRUE(tpl3.submit_sidechain_block(i3, 0, 0)); } diff --git a/tests/src/keccak_tests.cpp b/tests/src/keccak_tests.cpp index a390b8f..93b0883 100644 --- a/tests/src/keccak_tests.cpp +++ b/tests/src/keccak_tests.cpp @@ -90,4 +90,42 @@ TEST(keccak, hashing_bmi) } #endif +TEST(keccak, SHA3) +{ + auto check = [](const char* input, const char* expected_output) { + std::vector data; + ASSERT_TRUE(from_hex(input, strlen(input), data)); + + hash h; + + keccak_custom([&data](int offset) { return data[offset]; }, data.size(), h.h, HASH_SIZE, true); + + char buf[log::Stream::BUF_SIZE + 1]; + log::Stream s(buf); + s << h; + + ASSERT_EQ(memcmp(buf, expected_output, HASH_SIZE * 2), 0); + }; + + check("", "a7ffc6f8bf1ed76651c14756a061d662f580ff4de43b49fa82d80a4b80f8434a"); + + check("e9", "f0d04dd1e6cfc29a4460d521796852f25d9ef8d28b44ee91ff5b759d72c1e6d6"); + check("d477", "94279e8f5ccdf6e17f292b59698ab4e614dfe696a46c46da78305fc6a3146ab7"); + check("b053fa", "9d0ff086cd0ec06a682c51c094dc73abdc492004292344bd41b82a60498ccfdb"); + check("e7372105", "3a42b68ab079f28c4ca3c752296f279006c4fe78b1eb79d989777f051e4046ae"); + + check("989fc49594afc73405bacee4dbbe7135804f800368de39e2ea3bbec04e59c6c52752927ee3aa233ba0d8aab5410240f4c109d770c8c570777c928fce9a0bec9bc5156c821e204f0f14a9ab547e0319d3e758ae9e28eb2dbc3d9f7acf51bd52f41bf23aeb6d97b5780a35ba08b94965989744edd3b1d6d67ad26c68099af85f98d0f0e4fff9", "b10adeb6a9395a48788931d45a7b4e4f69300a76d8b716c40c614c3113a0f051"); + check("e5022f4c7dfe2dbd207105e2f27aaedd5a765c27c0bc60de958b49609440501848ccf398cf66dfe8dd7d131e04f1432f32827a057b8904d218e68ba3b0398038d755bd13d5f168cfa8a11ab34c0540873940c2a62eace3552dcd6953c683fdb29983d4e417078f1988c560c9521e6f8c78997c32618fc510db282a985f868f2d973f82351d11", "3293a4b9aeb8a65e1014d3847500ffc8241594e9c4564cbd7ce978bfa50767fe"); + check("b1f6076509938432145bb15dbe1a7b2e007934be5f753908b50fd24333455970a7429f2ffbd28bd6fe1804c4688311f318fe3fcd9f6744410243e115bcb00d7e039a4fee4c326c2d119c42abd2e8f4155a44472643704cc0bc72403b8a8ab0fd4d68e04a059d6e5ed45033b906326abb4eb4147052779bad6a03b55ca5bd8b140e131bed2dfada", "f82d9602b231d332d902cb6436b15aef89acc591cb8626233ced20c0a6e80d7a"); + check("56ea14d7fcb0db748ff649aaa5d0afdc2357528a9aad6076d73b2805b53d89e73681abfad26bee6c0f3d20215295f354f538ae80990d2281be6de0f6919aa9eb048c26b524f4d91ca87b54c0c54aa9b54ad02171e8bf31e8d158a9f586e92ffce994ecce9a5185cc80364d50a6f7b94849a914242fcb73f33a86ecc83c3403630d20650ddb8cd9c4", "4beae3515ba35ec8cbd1d94567e22b0d7809c466abfbafe9610349597ba15b45"); + + std::string s; + s.reserve(2000000); + + for (size_t i = 0; i < 1000000; ++i) { + s += "61"; + } + check(s.c_str(), "5c8875ae474a3634ba4fd55ec85bffd661f32aca75c6d699d0cdcb6c115891c1"); +} + } diff --git a/tests/src/pool_block_tests.cpp b/tests/src/pool_block_tests.cpp index 76d29a0..4e6c900 100644 --- a/tests/src/pool_block_tests.cpp +++ b/tests/src/pool_block_tests.cpp @@ -24,6 +24,7 @@ #include "side_chain.h" #include "p2p_server.h" #include "keccak.h" +#include "params.h" #include "gtest/gtest.h" #include @@ -246,12 +247,12 @@ TEST(pool_block, verify) mempool.add(tx); } - tpl.update( - data, - mempool, - Wallet("44MnN1f3Eto8DZYUWuE5XZNUtE3vcRzt2j6PzqWpPau34e6Cf4fAxt6X2MBmrm6F9YMEiMNjN6W4Shn4pLcfNAja621jwyg"), - Wallet("86eQxzSW4AZfvsWRSop755WZUsog6L3x32NRZukeeShnS4mBGVpcqQhS6pCNxj44usPKNwesZ45ooHyjDku6nVZdT3Q9qrz") - ); + Params params; + + params.m_miningWallet = Wallet("44MnN1f3Eto8DZYUWuE5XZNUtE3vcRzt2j6PzqWpPau34e6Cf4fAxt6X2MBmrm6F9YMEiMNjN6W4Shn4pLcfNAja621jwyg"); + params.m_subaddress = Wallet("86eQxzSW4AZfvsWRSop755WZUsog6L3x32NRZukeeShnS4mBGVpcqQhS6pCNxj44usPKNwesZ45ooHyjDku6nVZdT3Q9qrz"); + + tpl.update(data, mempool, ¶ms); std::vector blobs; uint64_t height; diff --git a/tests/src/util_tests.cpp b/tests/src/util_tests.cpp index c58e02d..9960864 100644 --- a/tests/src/util_tests.cpp +++ b/tests/src/util_tests.cpp @@ -17,6 +17,9 @@ #include "common.h" #include "util.h" +extern "C" { +#include "crypto-ops.h" +} #include "gtest/gtest.h" namespace p2pool { @@ -112,4 +115,129 @@ TEST(util, bsr) } } +TEST(util, onion) +{ + const std::string tests[] = { + "yucmgsbw7nknw7oi3bkuwudvc657g2xcqahhbjyewazusyytapqo4xid.onion", + "p2pool2giz2r5cpqicajwoazjcxkfujxswtk3jolfk2ubilhrkqam2id.onion", + "p2pseeds5qoenuuseyuqxhzzefzxpbhiq4z4h5hfbry5dxd5y2fwudyd.onion", + "testhrjytc63cnmcgff5tlcz2phd4sixeogfkjq6uinihwcs2awm3oad.onion", + "testsao5qh4qz4ne3iqytyve6a6ijbzmrok6wbauwxfzplv4g276elyd.onion", + "testaohgecnzvl6k25fvhkqaizc3vu6e6dxg62qgbtzqmoflnj5bk6id.onion", + "testgamrbpyy5e6kogd5kk4cdzvr6qzd6w3fj5eyxa5ccyaudwuwx3ad.onion", + "testdriz2m4xh6czzkjmbsnicqoyqyyiilhkogp3n6if7x5qaairetid.onion", + "testc7rluz33af3wlypoiwamwzti6gup3il2kqkf7ly7td5qggpvknyd.onion", + "testlxqch2dtemlwjc4bt3y6fj3vvknjksywugceibnhscybw4vlnlyd.onion", + "testbbx66qk4cgy342mm32jrod6k6zi4gu6bfvyjxsx3m6mf4khvuyqd.onion", + "testpita5yopwhs4utuk5ylfndzkijxv3lh7d6gjifxjg7oriuhn3mqd.onion", + "testdtmzbyzi47b677ocnoyi7w6oylcnyqh77pl2yv5q22qla7agz7qd.onion", + "testv3qobc33v3gfjnccs24eorkrtdzh5e4jbth5yag32sd5d24xazid.onion", + "testgwoazhovosqn2yfqaewkd6snynwm66qb5egcbenh3m54yi3ur4yd.onion", + "testu2oufhxgp6qqehf2ytpxqvmr3qm6dit3k7p3ixhnxjbxwqmfooyd.onion", + "testvi6s6dp5reui35lv6lnu3tzxmu4h3dofejjmmttr6ax3eshvefid.onion", + "testsap5p4lorjlvo4ovs6yxmn4lb3lgehsjyolqfcm7l53rqeswvnid.onion", + "testexfa567ampq3dkw5smfmofci6qvicl3niaoe6pneekcheqdsbfad.onion", + "testhnodrpe2qukswkp5554csphtq325pnnpm7uqs4ats5es7gcujhqd.onion", + "testneyu3idcjyvgmdylszcebkw5xsl5bnxyfwdu27fcbb2cm7pcs3id.onion", + "test5y4it65r5lnm3w4en4lj3bmy35nypmbgshkawt2h2ep3f75yuwyd.onion", + "testknxevfffltpt2iq5xioye3z52eoh4aefy3aegfmhogqf7w6xtnqd.onion", + "testshitk3xpt5aex3doyepmbvpsbzptjr6ujso6gahz4arryp7s6vyd.onion", + "testfin66dkzp7ozolfu7exjxi3cuck35xunt4a56xfanivs4xqsfbqd.onion", + "testec5noqmo2javrlye5higddskhvsialohrwwi2naoqkxrrwhucaid.onion", + "testbo2fncb7mblktg23a77clp4pdqfdje7g6axohcegle46bk64hnad.onion", + "testd7rzyebdtzpx3fjalkqmeuppbmqs4pbx5pxusyq57m73fyjwgxyd.onion", + "testmw3wweekyz7ui7uodlgq3zbv23hehkusrb257yerlpsizhylswyd.onion", + "testspzmswow3rc3x6ymbbsfuovcia3umhzribfcpnry7xw66lyjysad.onion", + "test635ior3cpu4hd4e4ogd3kzy2pugyv3gkfp3q5u4x2riojbhrvtid.onion", + "testn3vi7q5c5j5dqiq3hd3wtkazpxhfaxg2s6kxe6q75hvjnu62iiqd.onion", + "testjmo662avqobpfukplqt4bg5jd2dqu6rdr53a6jyju6jf72iavbyd.onion", + "testertyaowcvevhjdjwqr3jjvqkzzc2vrmp5hvd3iecukgjbla5xnqd.onion", + "testivnwlanaiqoxbcsmlsgpswpx72c5zifljfwta444mtkfk476pmad.onion", + "test7u4feuytc3gulkk2mdrmobkqjs6kc5tqxc32qv2v565ybm3kvsyd.onion", + "testmcndo5ahfvfnlxefpc3325drm4xfgtgx42hj7p7we7ppyursrbyd.onion", + "testtcr5xoqnirota5v5aonhn6gpzulewfbzb7kduld73jx353drjpyd.onion", + "testc4biea6pwrya56s2vtus33c3bhrx6pnt3gfwwwoclhkuujvcwhyd.onion", + "testbjeyc5bizi7ys7cjgklkpeq3bhadaukk6sx3r6pycfxx35ksipyd.onion", + "testvvumkf5b7k3vaagl54yujefrwsr7iuldkepzewwdiwyogkzzwyyd.onion", + "testiysaar7h25477y7ee7xhz4foyrjkmtoxbh3kiiazvuehlp7ubiid.onion", + "test7zh6zfacxeyrt3qkbelnfpf4sepmcypvl33vkbbeylbk3ddf4qad.onion", + "testebbxturu56qaxvlr7dqbrdjigga7mlmjanq4suc7j5h77h5iszyd.onion", + "testob7j77x3qmy6dd6ja6wvofhbedzloxae6rrxrf7hv5fpi2aotayd.onion", + "testgy24eidqv7wgaqtsucvqnagcyohpmzdmox2cpztd5eukmm6ubkid.onion", + "testtdslb2etamcxo7pklesyqxnidy6m5b5qu3yzcfhafajrnm5rotyd.onion", + "testha6wehnus7v4qb5cxbuf232fomz7ge6idrrenjewvmcl2fneffid.onion", + "testmp7u2cxnnjqsjcfz3ydd6gpiqbym5r5rxykqhz3h6ks6nsnthuad.onion", + "tests3xay2zjfkzzpfzl7s3kpnjftfwxmo7e3v5p657pfxbuqxt5e3id.onion", + "test6zfam4nauxtjkckvkd6fp55bjuikaj5wevftsfhdromu7wuj2pyd.onion", + "testssyxuohidxfvtma4wxpiot5qqkmukqcrjw7t2ngfltidzxo6eyid.onion", + "testmcfwamtj4adtz46izavwrddkp7fbu2swtapuyt5eyfupnztsd6qd.onion", + "testcycabwxf7em7hu2css5aax7up3opds6twra7hmpjnxi6fdmgs3yd.onion", + "testaev2vumiarnfrr4x4japjc2xtvvlp65gt4vwwqf3q4zolvprs5id.onion", + "testkynjqkj65t6wvuwajm55xzninrap4piqek5ao6jiwewvqwezalqd.onion", + "testnfxt6eemmiwhlme7ys7rj647jv46razwdf6whiwyjoruw4xceyad.onion", + "testrbutgvilidnf55thbo4pye4jn322ugmepc37celxvazal5uugoid.onion", + "test7xcn36xbr6raxldgh3kf2xk36gyor3hx3gzy2guhxzntxlvf3iid.onion", + "testsdjiq77anfffxus3utsmykucoqexpeojaklbmzhab3vow3ozfdqd.onion", + "testggm7oruqsw3sr2fd4nq4b3nh2fdd7rgw76p6gpz6pu6nsffv6byd.onion", + "testk5b5gjpsc5jkvqnd24aujh25ksvorjrcw457eremkynzoaxjrhad.onion", + "testc6mexahgqsbqkxoso5t77nitwwumqn5ucdzbsntgkvuwpxzv4mqd.onion", + "testcbsvi6v7h2avabs74osdnifiszdlygi4sljxohvuugf24hadjvid.onion", + "testfbhecs2llswxsgjuuqvq5boo6se3gni7ygjtbnagt4x5qt7sppid.onion", + "test7gjc7xb7ynoa5nzqrceqvo3fsotmtw2k5tj7ie2okvkmxuejobad.onion", + "testmskkbp3ikvsfvtfekm5hmjc4ou4hqlerx45qchm5yrwd56m6stad.onion", + "testv52txxzvdmi5kuuhwbzyrbzrhqnxr6ltvycgavt3wb5uyxcbrqqd.onion", + "test7nsiyeg2hsmuvi7uhqbaoygf4lfa6ysihaqicrkzwulp7shsh3ad.onion", + "testu2p6psiaw7qnhvi3v7tjgawbf6zpmjajmbfdofimv3wpniaqleyd.onion", + "testkqcq6qg3yibtnwxgm2qceti2k74suvgqet2wdqmcoluzu4b3fkqd.onion", + "test2i4762wcfcl5wgjf7ofnkorsq6l6buuet7ldddezeqgd372sysyd.onion", + "testftqmujq6h5rhey73mmqlhpuh5zs5cipqqxwbzihiqgw6gnfpp6id.onion", + "test5hua7kwnsnv3uiwathv3khq4nmhbwsayl6uzj2gs6pmimork2iid.onion", + "test7tdanour43exhioddlji7jgsgiibi5hg3jhu47wxaszbvtwat2ad.onion", + "testd26wxq4q44wmvplnngh5onomax5f3brjrocazz56mj7qylogy6id.onion", + "testyuwguibjovaw2mmzvpxqh2r3dhjddeaez6242pmro37gxivknrqd.onion", + "testi5lokj4wrawmihfqmic74wac6a4asy6o7erzxjosuyedzsscdeid.onion", + "testbuwdono434jdgv76u7jjczd7sweu36ga55vo66tdcsjscov5dfad.onion", + "testor6cqxswpeomtnsqhc73knjftqchalz5whatqddaxguwm5s2fhad.onion", + "test2tz52bpiib5zbxt2bfg77ika54cfvz3p6ovkqbqiyu6d5ecaetyd.onion", + "testc5wvnm55zfk3ihkebuf4kxkhaandcqwizkehb6fpfgs2f4tfxvqd.onion", + "testpxuiq5dgkvhwynsyzrtdsxtthrt4lhjtb2egs34ypwzcvsrgbhqd.onion", + "testgpc7h7f6tmo6lrz6to5svh6gw2mxgelbo5du6kakuuco5oyu2pad.onion", + "test7vlusiy2t5e2dpwf4ttlajpefxqpbj274l25iml3ltjf6dwgzhyd.onion", + "testcaq5d3ehqqwdkdcux4db57lbbcfcl5wfebg7jxwl5gpv7aavxfyd.onion", + "testaqtkrclsz3ehn4i3i4maqoxniivmdl5gnefelzva6ts2yrx5vyyd.onion", + "testgkfz4a2ir44r6pacf3fvyrldjk6n4u3odbyveseo2jatze6euxyd.onion", + "testsrinaqfbsh22gjlkabcwn4rbypsb3nmtutqnjr37tvqmhdc2ysqd.onion", + "testyo5bximqrzkkeppmr5mopciote6wx6wcnus6wnuanl5c2zztuwad.onion", + "testmgjjfxt5frdpvfqchrgmoohxnba63q6wt3spva7kvgiae36jjuqd.onion", + "testn2qrokp2v4vtjjdrrlzosa6iz2v322ntsrguj6t4d2hguouhpzad.onion", + "test3njq3v3rp4sy5uhyulvl3eus34hev4cybkernrgvlmqonadxalid.onion", + "testylx25ok4zwlm5myjgvlbudigeqxyvxy2hhk7kvwub4uqsfaawbyd.onion", + "test4zcne6f7w6xstprxyhwp2gbl4exvvltewrnjhtsss4bu74faokqd.onion", + "testddleenrfxcbpsqrugj3ugnpjgd2xeneznzopzcndqebmavo65sqd.onion", + "testoq3eqrragrjpwiegwokzukqd2p73ffy6dcgnc6nerxtodsfotkad.onion", + "testyivcoaauwqekzux66r2ao4j2hizka4kdeyb5lyvmdzsrw3uybeid.onion", + "testz5tgehn7i3myjdobvjlnbax7yd2e3ra27gyiprzicvksm5r6i5yd.onion", + "testc4vbagkuxee3pkgvlb7w2pqgdq3rlaru2ipgpox5xzssejhfizad.onion", + "testq4ryujfitfcxabcjde6m7uqdztdep6mzd32e4wbtqna4jyponaad.onion", + "test2muitbvopcoducxb6d5bqry5dmxdatupvh34anzjdeav6xiigead.onion", + "test76ais6k5t4bmap4uyl2eleh6o4g423cxuvifcoke4gtgd6pjtpqd.onion", + }; + + for (const std::string& address : tests) { + const hash h = from_onion_v3(address); + ASSERT_TRUE(!h.empty()); + + const std::string s = to_onion_v3(h); + ASSERT_EQ(s, address); + } + + ASSERT_TRUE(from_onion_v3("tooshort.onion").empty()); + ASSERT_TRUE(from_onion_v3("inval1dcharacter777777777777777777777777777777777777777d.onion").empty()); + ASSERT_TRUE(from_onion_v3("wrongchecksum777777777777777777777777777777777777777777d.onion").empty()); + ASSERT_TRUE(from_onion_v3("yucmgsbw7nknw7oi3bkuwudvc657g2xcqahhbjyewazusyytapqo4xid.xnion").empty()); + + // Invalid pubkey + ASSERT_TRUE(from_onion_v3("civ5tgldg3yx73ytse6hvvk3nm6q3zctbqvytpszihm35b33ze73kxad.onion").empty()); +} + }