From 02d887c2e58bd8e66ef8823e59669439d448bfec Mon Sep 17 00:00:00 2001 From: Lee Clagett Date: Wed, 13 Nov 2019 14:12:32 +0000 Subject: [PATCH] Adding Dandelion++ support to public networks: - New flag in NOTIFY_NEW_TRANSACTION to indicate stem mode - Stem loops detected in tx_pool.cpp - Embargo timeout for a blackhole attack during stem phase --- src/blockchain_db/blockchain_db.cpp | 26 +- src/blockchain_db/blockchain_db.h | 8 +- src/crypto/duration.h | 70 + src/cryptonote_basic/verification_context.h | 7 +- src/cryptonote_config.h | 7 +- src/cryptonote_core/cryptonote_core.cpp | 5 +- src/cryptonote_core/tx_pool.cpp | 115 +- .../cryptonote_protocol_defs.h | 2 + .../cryptonote_protocol_handler.h | 2 +- .../cryptonote_protocol_handler.inl | 59 +- .../cryptonote_protocol_handler_common.h | 4 +- src/cryptonote_protocol/enums.h | 5 +- src/cryptonote_protocol/levin_notify.cpp | 210 ++- src/cryptonote_protocol/levin_notify.h | 3 +- src/p2p/net_node.h | 2 +- src/p2p/net_node.inl | 11 +- src/p2p/net_node_common.h | 4 +- src/rpc/core_rpc_server.cpp | 10 +- src/rpc/daemon_handler.cpp | 10 +- src/rpc/daemon_handler.h | 2 +- tests/core_tests/chaingen.h | 7 +- tests/core_tests/chaingen_main.cpp | 1 + tests/core_tests/tx_pool.cpp | 29 +- tests/core_tests/tx_pool.h | 9 + tests/unit_tests/levin.cpp | 1241 +++++++++++++++-- 25 files changed, 1620 insertions(+), 229 deletions(-) create mode 100644 src/crypto/duration.h diff --git a/src/blockchain_db/blockchain_db.cpp b/src/blockchain_db/blockchain_db.cpp index 1a6a19da5..01faf43c4 100644 --- a/src/blockchain_db/blockchain_db.cpp +++ b/src/blockchain_db/blockchain_db.cpp @@ -53,9 +53,7 @@ bool matches_category(relay_method method, relay_category category) noexcept case relay_category::all: return true; case relay_category::relayable: - if (method == relay_method::none) - return false; - return true; + return method != relay_method::none; case relay_category::broadcasted: case relay_category::legacy: break; @@ -65,6 +63,7 @@ bool matches_category(relay_method method, relay_category category) noexcept { default: case relay_method::local: + case relay_method::stem: return false; case relay_method::block: case relay_method::fluff: @@ -80,6 +79,7 @@ void txpool_tx_meta_t::set_relay_method(relay_method method) noexcept kept_by_block = 0; do_not_relay = 0; is_local = 0; + dandelionpp_stem = 0; switch (method) { @@ -92,6 +92,9 @@ void txpool_tx_meta_t::set_relay_method(relay_method method) noexcept default: case relay_method::fluff: break; + case relay_method::stem: + dandelionpp_stem = 1; + break; case relay_method::block: kept_by_block = 1; break; @@ -106,9 +109,26 @@ relay_method txpool_tx_meta_t::get_relay_method() const noexcept return relay_method::none; if (is_local) return relay_method::local; + if (dandelionpp_stem) + return relay_method::stem; return relay_method::fluff; } +bool txpool_tx_meta_t::upgrade_relay_method(relay_method method) noexcept +{ + static_assert(relay_method::none < relay_method::local, "bad relay_method value"); + static_assert(relay_method::local < relay_method::stem, "bad relay_method value"); + static_assert(relay_method::stem < relay_method::fluff, "bad relay_method value"); + static_assert(relay_method::fluff < relay_method::block, "bad relay_method value"); + + if (get_relay_method() < method) + { + set_relay_method(method); + return true; + } + return false; +} + const command_line::arg_descriptor arg_db_sync_mode = { "db-sync-mode" , "Specify sync option, using format [safe|fast|fastest]:[sync|async]:[[blocks]|[bytes]]." diff --git a/src/blockchain_db/blockchain_db.h b/src/blockchain_db/blockchain_db.h index d3a218365..3e2387da4 100644 --- a/src/blockchain_db/blockchain_db.h +++ b/src/blockchain_db/blockchain_db.h @@ -160,7 +160,7 @@ struct txpool_tx_meta_t uint64_t max_used_block_height; uint64_t last_failed_height; uint64_t receive_time; - uint64_t last_relayed_time; + uint64_t last_relayed_time; //!< If Dandelion++ stem, randomized embargo timestamp. Otherwise, last relayed timestmap. // 112 bytes uint8_t kept_by_block; uint8_t relayed; @@ -168,13 +168,17 @@ struct txpool_tx_meta_t uint8_t double_spend_seen: 1; uint8_t pruned: 1; uint8_t is_local: 1; - uint8_t bf_padding: 5; + uint8_t dandelionpp_stem : 1; + uint8_t bf_padding: 4; uint8_t padding[76]; // till 192 bytes void set_relay_method(relay_method method) noexcept; relay_method get_relay_method() const noexcept; + //! \return True if `get_relay_method()` now returns `method`. + bool upgrade_relay_method(relay_method method) noexcept; + //! See `relay_category` description bool matches(const relay_category category) const noexcept { diff --git a/src/crypto/duration.h b/src/crypto/duration.h new file mode 100644 index 000000000..493874288 --- /dev/null +++ b/src/crypto/duration.h @@ -0,0 +1,70 @@ +// Copyright (c) 2020, 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. + +#pragma once + +#include +#include "crypto/crypto.h" + +namespace crypto +{ + //! Generate poisson distributed values in discrete `D` time units. + template + struct random_poisson_duration + { + using result_type = D; //!< std::chrono::duration time unit precision + using rep = typename result_type::rep; //!< Type used to represent duration value + + //! \param average for generated durations + explicit random_poisson_duration(result_type average) + : dist(average.count() < 0 ? 0 : average.count()) + {} + + //! Generate a crypto-secure random duration + result_type operator()() + { + crypto::random_device rand{}; + return result_type{dist(rand)}; + } + + private: + std::poisson_distribution dist; + }; + + /* A custom duration is used for subsecond precision because of the + variance. If 5000 milliseconds is given, 95% of the values fall between + 4859ms-5141ms in 1ms increments (not enough time variance). Providing 1/4 + seconds would yield 95% of the values between 3s-7.25s in 1/4s + increments. */ + + //! Generate random durations with 1 second precision + using random_poisson_seconds = random_poisson_duration; + //! Generate random duration with 1/4 second precision + using random_poisson_subseconds = + random_poisson_duration>>; +} diff --git a/src/cryptonote_basic/verification_context.h b/src/cryptonote_basic/verification_context.h index 4d49b692c..ec5f604a5 100644 --- a/src/cryptonote_basic/verification_context.h +++ b/src/cryptonote_basic/verification_context.h @@ -29,6 +29,9 @@ // Parts of this file are originally copyright (c) 2012-2013 The Cryptonote developers #pragma once + +#include "cryptonote_protocol/enums.h" + namespace cryptonote { /************************************************************************/ @@ -36,7 +39,9 @@ namespace cryptonote /************************************************************************/ struct tx_verification_context { - bool m_should_be_relayed; + static_assert(unsigned(relay_method::none) == 0, "default m_relay initialization is not to relay_method::none"); + + relay_method m_relay; // gives indication on how tx should be relayed (if at all) bool m_verifivation_failed; //bad tx, should drop connection bool m_verifivation_impossible; //the transaction is related with an alternative blockchain bool m_added_to_pool; diff --git a/src/cryptonote_config.h b/src/cryptonote_config.h index 66af46a5f..81dc15dee 100644 --- a/src/cryptonote_config.h +++ b/src/cryptonote_config.h @@ -102,7 +102,12 @@ #define CRYPTONOTE_MEMPOOL_TX_FROM_ALT_BLOCK_LIVETIME 604800 //seconds, one week -#define CRYPTONOTE_DANDELIONPP_FLUSH_AVERAGE 5 // seconds +#define CRYPTONOTE_DANDELIONPP_STEMS 2 // number of outgoing stem connections per epoch +#define CRYPTONOTE_DANDELIONPP_FLUFF_PROBABILITY 10 // out of 100 +#define CRYPTONOTE_DANDELIONPP_MIN_EPOCH 10 // minutes +#define CRYPTONOTE_DANDELIONPP_EPOCH_RANGE 30 // seconds +#define CRYPTONOTE_DANDELIONPP_FLUSH_AVERAGE 5 // seconds average for poisson distributed fluff flush +#define CRYPTONOTE_DANDELIONPP_EMBARGO_AVERAGE 173 // seconds (see tx_pool.cpp for more info) // see src/cryptonote_protocol/levin_notify.cpp #define CRYPTONOTE_NOISE_MIN_EPOCH 5 // minutes diff --git a/src/cryptonote_core/cryptonote_core.cpp b/src/cryptonote_core/cryptonote_core.cpp index 7fb232ad2..10bbff457 100644 --- a/src/cryptonote_core/cryptonote_core.cpp +++ b/src/cryptonote_core/cryptonote_core.cpp @@ -1284,6 +1284,7 @@ namespace cryptonote break; case relay_method::block: case relay_method::fluff: + case relay_method::stem: public_req.txs.push_back(std::move(std::get<1>(tx))); break; } @@ -1295,9 +1296,9 @@ namespace cryptonote re-relaying public and private _should_ be acceptable here. */ const boost::uuids::uuid source = boost::uuids::nil_uuid(); if (!public_req.txs.empty()) - get_protocol()->relay_transactions(public_req, source, epee::net_utils::zone::public_); + get_protocol()->relay_transactions(public_req, source, epee::net_utils::zone::public_, relay_method::fluff); if (!private_req.txs.empty()) - get_protocol()->relay_transactions(private_req, source, epee::net_utils::zone::invalid); + get_protocol()->relay_transactions(private_req, source, epee::net_utils::zone::invalid, relay_method::local); } return true; } diff --git a/src/cryptonote_core/tx_pool.cpp b/src/cryptonote_core/tx_pool.cpp index d7fc89d61..c49a3dabc 100644 --- a/src/cryptonote_core/tx_pool.cpp +++ b/src/cryptonote_core/tx_pool.cpp @@ -46,6 +46,7 @@ #include "warnings.h" #include "common/perf_timer.h" #include "crypto/hash.h" +#include "crypto/duration.h" #undef MONERO_DEFAULT_LOG_CATEGORY #define MONERO_DEFAULT_LOG_CATEGORY "txpool" @@ -58,6 +59,29 @@ namespace cryptonote { namespace { + /*! The Dandelion++ has formula for calculating the average embargo timeout: + (-k*(k-1)*hop)/(2*log(1-ep)) + where k is the number of hops before this node and ep is the probability + that one of the k hops hits their embargo timer, and hop is the average + time taken between hops. So decreasing ep will make it more probable + that "this" node is the first to expire the embargo timer. Increasing k + will increase the number of nodes that will be "hidden" as a prior + recipient of the tx. + + As example, k=5 and ep=0.1 means "this" embargo timer has a 90% + probability of being the first to expire amongst 5 nodes that saw the + tx before "this" one. These values are independent to the fluff + probability, but setting a low k with a low p (fluff probability) is + not ideal since a blackhole is more likely to reveal earlier nodes in + the chain. + + This value was calculated with k=10, ep=0.10, and hop = 175 ms. A + testrun from a recent Intel laptop took ~80ms to + receive+parse+proces+send transaction. At least 50ms will be added to + the latency if crossing an ocean. So 175ms is the fudge factor for + a single hop with 173s being the embargo timer. */ + constexpr const std::chrono::seconds dandelionpp_embargo_average{CRYPTONOTE_DANDELIONPP_EMBARGO_AVERAGE}; + //TODO: constants such as these should at least be in the header, // but probably somewhere more accessible to the rest of the // codebase. As it stands, it is at best nontrivial to test @@ -262,34 +286,51 @@ namespace cryptonote } }else { - //update transactions container - meta.weight = tx_weight; - meta.fee = fee; - meta.max_used_block_id = max_used_block_id; - meta.max_used_block_height = max_used_block_height; - meta.last_failed_height = 0; - meta.last_failed_id = null_hash; - meta.receive_time = receive_time; - meta.last_relayed_time = time(NULL); - meta.relayed = relayed; - meta.set_relay_method(tx_relay); - meta.double_spend_seen = false; - meta.pruned = tx.pruned; - meta.bf_padding = 0; - memset(meta.padding, 0, sizeof(meta.padding)); - try { if (kept_by_block) m_parsed_tx_cache.insert(std::make_pair(id, tx)); CRITICAL_REGION_LOCAL1(m_blockchain); LockedTXN lock(m_blockchain.get_db()); - m_blockchain.remove_txpool_tx(id); - if (!insert_key_images(tx, id, tx_relay)) - return false; - m_blockchain.add_txpool_tx(id, blob, meta); - m_txs_by_fee_and_receive_time.emplace(std::pair(fee / (double)(tx_weight ? tx_weight : 1), receive_time), id); + const bool existing_tx = m_blockchain.get_txpool_tx_meta(id, meta); + if (existing_tx) + { + /* If Dandelion++ loop. Do not use txes in the `local` state in the + loop detection - txes in that state should be outgoing over i2p/tor + then routed back via public dandelion++ stem. Pretend to be + another stem node in that situation, a loop over the public + network hasn't been hit yet. */ + if (tx_relay == relay_method::stem && meta.dandelionpp_stem) + tx_relay = relay_method::fluff; + } + else + meta.set_relay_method(relay_method::none); + + if (meta.upgrade_relay_method(tx_relay) || !existing_tx) // synchronize with embargo timer or stem/fluff out-of-order messages + { + //update transactions container + meta.last_relayed_time = std::numeric_limits::max(); + meta.receive_time = receive_time; + meta.weight = tx_weight; + meta.fee = fee; + meta.max_used_block_id = max_used_block_id; + meta.max_used_block_height = max_used_block_height; + meta.last_failed_height = 0; + meta.last_failed_id = null_hash; + meta.relayed = relayed; + meta.double_spend_seen = false; + meta.pruned = tx.pruned; + meta.bf_padding = 0; + memset(meta.padding, 0, sizeof(meta.padding)); + + if (!insert_key_images(tx, id, tx_relay)) + return false; + + m_blockchain.remove_txpool_tx(id); + m_blockchain.add_txpool_tx(id, blob, meta); + m_txs_by_fee_and_receive_time.emplace(std::pair(fee / (double)(tx_weight ? tx_weight : 1), receive_time), id); + } lock.commit(); } catch (const std::exception &e) @@ -299,8 +340,9 @@ namespace cryptonote } tvc.m_added_to_pool = true; - if(meta.fee > 0 && tx_relay != relay_method::none) - tvc.m_should_be_relayed = true; + static_assert(unsigned(relay_method::none) == 0, "expected relay_method::none value to be zero"); + if(meta.fee > 0) + tvc.m_relay = tx_relay; } tvc.m_verifivation_failed = false; @@ -553,7 +595,7 @@ namespace cryptonote td.last_failed_height = meta.last_failed_height; td.last_failed_id = meta.last_failed_id; td.receive_time = meta.receive_time; - td.last_relayed_time = meta.last_relayed_time; + td.last_relayed_time = meta.dandelionpp_stem ? 0 : meta.last_relayed_time; td.relayed = meta.relayed; td.do_not_relay = meta.do_not_relay; td.double_spend_seen = meta.double_spend_seen; @@ -686,8 +728,13 @@ namespace cryptonote txs.reserve(m_blockchain.get_txpool_tx_count()); m_blockchain.for_all_txpool_txes([this, now, &txs](const crypto::hash &txid, const txpool_tx_meta_t &meta, const cryptonote::blobdata *){ // 0 fee transactions are never relayed - if(!meta.pruned && meta.fee > 0 && !meta.do_not_relay && now - meta.last_relayed_time > get_relay_delay(now, meta.receive_time)) + if(!meta.pruned && meta.fee > 0 && !meta.do_not_relay) { + if (!meta.dandelionpp_stem && now - meta.last_relayed_time <= get_relay_delay(now, meta.receive_time)) + return true; + if (meta.dandelionpp_stem && meta.last_relayed_time < now) // for dandelion++ stem, this value is the embargo timeout + return true; + // if the tx is older than half the max lifetime, we don't re-relay it, to avoid a problem // mentioned by smooth where nodes would flush txes at slightly different times, causing // flushed txes to be re-added when received from a node which was just about to flush it @@ -712,9 +759,11 @@ namespace cryptonote //--------------------------------------------------------------------------------- void tx_memory_pool::set_relayed(const epee::span hashes, const relay_method method) { + crypto::random_poisson_seconds embargo_duration{dandelionpp_embargo_average}; + const auto now = std::chrono::system_clock::now(); + CRITICAL_REGION_LOCAL(m_transactions_lock); CRITICAL_REGION_LOCAL1(m_blockchain); - const time_t now = time(NULL); LockedTXN lock(m_blockchain.get_db()); for (const auto& hash : hashes) { @@ -723,9 +772,15 @@ namespace cryptonote txpool_tx_meta_t meta; if (m_blockchain.get_txpool_tx_meta(hash, meta)) { + // txes can be received as "stem" or "fluff" in either order + meta.upgrade_relay_method(method); meta.relayed = true; - meta.last_relayed_time = now; - meta.set_relay_method(method); + + if (meta.dandelionpp_stem) + meta.last_relayed_time = std::chrono::system_clock::to_time_t(now + embargo_duration()); + else + meta.last_relayed_time = std::chrono::system_clock::to_time_t(now); + m_blockchain.update_txpool_tx(hash, meta); } } @@ -910,7 +965,7 @@ namespace cryptonote txi.receive_time = include_sensitive_data ? meta.receive_time : 0; txi.relayed = meta.relayed; // In restricted mode we do not include this data: - txi.last_relayed_time = include_sensitive_data ? meta.last_relayed_time : 0; + txi.last_relayed_time = (include_sensitive_data && !meta.dandelionpp_stem) ? meta.last_relayed_time : 0; txi.do_not_relay = meta.do_not_relay; txi.double_spend_seen = meta.double_spend_seen; tx_infos.push_back(std::move(txi)); @@ -962,7 +1017,7 @@ namespace cryptonote txi.last_failed_block_hash = meta.last_failed_id; txi.receive_time = meta.receive_time; txi.relayed = meta.relayed; - txi.last_relayed_time = meta.last_relayed_time; + txi.last_relayed_time = meta.dandelionpp_stem ? 0 : meta.last_relayed_time; txi.do_not_relay = meta.do_not_relay; txi.double_spend_seen = meta.double_spend_seen; tx_infos.push_back(txi); diff --git a/src/cryptonote_protocol/cryptonote_protocol_defs.h b/src/cryptonote_protocol/cryptonote_protocol_defs.h index f809bff74..76b57afd3 100644 --- a/src/cryptonote_protocol/cryptonote_protocol_defs.h +++ b/src/cryptonote_protocol/cryptonote_protocol_defs.h @@ -197,10 +197,12 @@ namespace cryptonote { std::vector txs; std::string _; // padding + bool dandelionpp_fluff; //zero initialization defaults to stem mode BEGIN_KV_SERIALIZE_MAP() KV_SERIALIZE(txs) KV_SERIALIZE(_) + KV_SERIALIZE_OPT(dandelionpp_fluff, true) // backwards compatible mode is fluff END_KV_SERIALIZE_MAP() }; typedef epee::misc_utils::struct_init request; diff --git a/src/cryptonote_protocol/cryptonote_protocol_handler.h b/src/cryptonote_protocol/cryptonote_protocol_handler.h index 2664716a8..e6479b941 100644 --- a/src/cryptonote_protocol/cryptonote_protocol_handler.h +++ b/src/cryptonote_protocol/cryptonote_protocol_handler.h @@ -129,7 +129,7 @@ namespace cryptonote //----------------- i_bc_protocol_layout --------------------------------------- virtual bool relay_block(NOTIFY_NEW_BLOCK::request& arg, cryptonote_connection_context& exclude_context); - virtual bool relay_transactions(NOTIFY_NEW_TRANSACTIONS::request& arg, const boost::uuids::uuid& source, epee::net_utils::zone zone); + virtual bool relay_transactions(NOTIFY_NEW_TRANSACTIONS::request& arg, const boost::uuids::uuid& source, epee::net_utils::zone zone, relay_method tx_relay); //---------------------------------------------------------------------------------- //bool get_payload_sync_data(HANDSHAKE_DATA::request& hshd, cryptonote_connection_context& context); bool should_drop_connection(cryptonote_connection_context& context, uint32_t next_stripe); diff --git a/src/cryptonote_protocol/cryptonote_protocol_handler.inl b/src/cryptonote_protocol/cryptonote_protocol_handler.inl index 3aacce421..ca06fe51c 100644 --- a/src/cryptonote_protocol/cryptonote_protocol_handler.inl +++ b/src/cryptonote_protocol/cryptonote_protocol_handler.inl @@ -926,29 +926,60 @@ namespace cryptonote return 1; } - std::vector newtxs; - newtxs.reserve(arg.txs.size()); - for (size_t i = 0; i < arg.txs.size(); ++i) + relay_method tx_relay; + std::vector stem_txs{}; + std::vector fluff_txs{}; + if (arg.dandelionpp_fluff) { - cryptonote::tx_verification_context tvc{}; - m_core.handle_incoming_tx({arg.txs[i], crypto::null_hash}, tvc, relay_method::fluff, true); - if(tvc.m_verifivation_failed) + tx_relay = relay_method::fluff; + fluff_txs.reserve(arg.txs.size()); + } + else + { + tx_relay = relay_method::stem; + stem_txs.reserve(arg.txs.size()); + } + + for (auto& tx : arg.txs) + { + tx_verification_context tvc{}; + if (!m_core.handle_incoming_tx({tx, crypto::null_hash}, tvc, tx_relay, true)) { LOG_PRINT_CCONTEXT_L1("Tx verification failed, dropping connection"); drop_connection(context, false, false); return 1; } - if(tvc.m_should_be_relayed) - newtxs.push_back(std::move(arg.txs[i])); + + switch (tvc.m_relay) + { + case relay_method::local: + case relay_method::stem: + stem_txs.push_back(std::move(tx)); + break; + case relay_method::block: + case relay_method::fluff: + fluff_txs.push_back(std::move(tx)); + break; + default: + case relay_method::none: + break; + } } - arg.txs = std::move(newtxs); - if(arg.txs.size()) + if (!stem_txs.empty()) { //TODO: add announce usage here - relay_transactions(arg, context.m_connection_id, context.m_remote_address.get_zone()); + arg.dandelionpp_fluff = false; + arg.txs = std::move(stem_txs); + relay_transactions(arg, context.m_connection_id, context.m_remote_address.get_zone(), relay_method::stem); + } + if (!fluff_txs.empty()) + { + //TODO: add announce usage here + arg.dandelionpp_fluff = true; + arg.txs = std::move(fluff_txs); + relay_transactions(arg, context.m_connection_id, context.m_remote_address.get_zone(), relay_method::fluff); } - return 1; } //------------------------------------------------------------------------------------------------------------------------ @@ -2387,14 +2418,14 @@ skip: } //------------------------------------------------------------------------------------------------------------------------ template - bool t_cryptonote_protocol_handler::relay_transactions(NOTIFY_NEW_TRANSACTIONS::request& arg, const boost::uuids::uuid& source, epee::net_utils::zone zone) + bool t_cryptonote_protocol_handler::relay_transactions(NOTIFY_NEW_TRANSACTIONS::request& arg, const boost::uuids::uuid& source, epee::net_utils::zone zone, relay_method tx_relay) { /* Push all outgoing transactions to this function. The behavior needs to identify how the transaction is going to be relayed, and then update the local mempool before doing the relay. The code was already updating the DB twice on received transactions - it is difficult to workaround this due to the internal design. */ - return m_p2p->send_txs(std::move(arg.txs), zone, source, m_core) != epee::net_utils::zone::invalid; + return m_p2p->send_txs(std::move(arg.txs), zone, source, m_core, tx_relay) != epee::net_utils::zone::invalid; } //------------------------------------------------------------------------------------------------------------------------ template diff --git a/src/cryptonote_protocol/cryptonote_protocol_handler_common.h b/src/cryptonote_protocol/cryptonote_protocol_handler_common.h index 978a9ebf3..11184299d 100644 --- a/src/cryptonote_protocol/cryptonote_protocol_handler_common.h +++ b/src/cryptonote_protocol/cryptonote_protocol_handler_common.h @@ -41,7 +41,7 @@ namespace cryptonote struct i_cryptonote_protocol { virtual bool relay_block(NOTIFY_NEW_BLOCK::request& arg, cryptonote_connection_context& exclude_context)=0; - virtual bool relay_transactions(NOTIFY_NEW_TRANSACTIONS::request& arg, const boost::uuids::uuid& source, epee::net_utils::zone zone)=0; + virtual bool relay_transactions(NOTIFY_NEW_TRANSACTIONS::request& arg, const boost::uuids::uuid& source, epee::net_utils::zone zone, relay_method tx_relay)=0; //virtual bool request_objects(NOTIFY_REQUEST_GET_OBJECTS::request& arg, cryptonote_connection_context& context)=0; }; @@ -54,7 +54,7 @@ namespace cryptonote { return false; } - virtual bool relay_transactions(NOTIFY_NEW_TRANSACTIONS::request& arg, const boost::uuids::uuid& source, epee::net_utils::zone zone) + virtual bool relay_transactions(NOTIFY_NEW_TRANSACTIONS::request& arg, const boost::uuids::uuid& source, epee::net_utils::zone zone, relay_method tx_relay) { return false; } diff --git a/src/cryptonote_protocol/enums.h b/src/cryptonote_protocol/enums.h index 2ec622d94..a3a52b83f 100644 --- a/src/cryptonote_protocol/enums.h +++ b/src/cryptonote_protocol/enums.h @@ -37,7 +37,8 @@ namespace cryptonote { none = 0, //!< Received via RPC with `do_not_relay` set local, //!< Received via RPC; trying to send over i2p/tor, etc. - block, //!< Received in block, takes precedence over others - fluff //!< Received/sent over public networks + stem, //!< Received/send over network using Dandelion++ stem + fluff, //!< Received/sent over network using Dandelion++ fluff + block //!< Received in block, takes precedence over others }; } diff --git a/src/cryptonote_protocol/levin_notify.cpp b/src/cryptonote_protocol/levin_notify.cpp index 428b739bc..127801092 100644 --- a/src/cryptonote_protocol/levin_notify.cpp +++ b/src/cryptonote_protocol/levin_notify.cpp @@ -30,6 +30,7 @@ #include #include +#include #include #include #include @@ -38,8 +39,10 @@ #include "common/expect.h" #include "common/varint.h" #include "cryptonote_config.h" -#include "crypto/random.h" +#include "crypto/crypto.h" +#include "crypto/duration.h" #include "cryptonote_basic/connection_context.h" +#include "cryptonote_core/i_core_events.h" #include "cryptonote_protocol/cryptonote_protocol_defs.h" #include "net/dandelionpp.h" #include "p2p/net_node.h" @@ -61,11 +64,14 @@ namespace levin { namespace { - constexpr std::size_t connection_id_reserve_size = 100; + constexpr const std::size_t connection_id_reserve_size = 100; constexpr const std::chrono::minutes noise_min_epoch{CRYPTONOTE_NOISE_MIN_EPOCH}; constexpr const std::chrono::seconds noise_epoch_range{CRYPTONOTE_NOISE_EPOCH_RANGE}; + constexpr const std::chrono::minutes dandelionpp_min_epoch{CRYPTONOTE_DANDELIONPP_MIN_EPOCH}; + constexpr const std::chrono::seconds dandelionpp_epoch_range{CRYPTONOTE_DANDELIONPP_EPOCH_RANGE}; + constexpr const std::chrono::seconds noise_min_delay{CRYPTONOTE_NOISE_MIN_DELAY}; constexpr const std::chrono::seconds noise_delay_range{CRYPTONOTE_NOISE_DELAY_RANGE}; @@ -83,22 +89,8 @@ namespace levin connections (Dandelion++ makes similar assumptions in its stem algorithm). The randomization yields 95% values between 1s-4s in 1/4s increments. */ - constexpr const fluff_stepsize fluff_average_out{fluff_stepsize{fluff_average_in} / 2}; - - class random_poisson - { - std::poisson_distribution dist; - public: - explicit random_poisson(fluff_stepsize average) - : dist(average.count() < 0 ? 0 : average.count()) - {} - - fluff_stepsize operator()() - { - crypto::random_device rand{}; - return fluff_stepsize{dist(rand)}; - } - }; + using fluff_duration = crypto::random_poisson_subseconds::result_type; + constexpr const fluff_duration fluff_average_out{fluff_duration{fluff_average_in} / 2}; /*! Select a randomized duration from 0 to `range`. The precision will be to the systems `steady_clock`. As an example, supplying 3 seconds to this @@ -132,10 +124,11 @@ namespace levin return outs; } - std::string make_tx_payload(std::vector&& txs, const bool pad) + std::string make_tx_payload(std::vector&& txs, const bool pad, const bool fluff) { NOTIFY_NEW_TRANSACTIONS::request request{}; request.txs = std::move(txs); + request.dandelionpp_fluff = fluff; if (pad) { @@ -172,9 +165,9 @@ namespace levin return fullBlob; } - bool make_payload_send_txs(connections& p2p, std::vector&& txs, const boost::uuids::uuid& destination, const bool pad) + bool make_payload_send_txs(connections& p2p, std::vector&& txs, const boost::uuids::uuid& destination, const bool pad, const bool fluff) { - const cryptonote::blobdata blob = make_tx_payload(std::move(txs), pad); + const cryptonote::blobdata blob = make_tx_payload(std::move(txs), pad, fluff); p2p.for_connection(destination, [&blob](detail::p2p_context& context) { on_levin_traffic(context, true, true, false, blob.size(), get_command_from_message(blob)); return true; @@ -251,7 +244,8 @@ namespace levin flush_time(std::chrono::steady_clock::time_point::max()), connection_count(0), is_public(is_public), - pad_txs(pad_txs) + pad_txs(pad_txs), + fluffing(false) { for (std::size_t count = 0; !noise.empty() && count < CRYPTONOTE_NOISE_CHANNELS; ++count) channels.emplace_back(io_service); @@ -268,6 +262,7 @@ namespace levin std::atomic connection_count; //!< Only update in strand, can be read at any time const bool is_public; //!< Zone is public ipv4/ipv6 connections const bool pad_txs; //!< Pad txs to the next boundary for privacy + bool fluffing; //!< Zone is in Dandelion++ fluff epoch }; } // detail @@ -362,10 +357,11 @@ namespace levin return true; }); + // Always send txs in stem mode over i2p/tor, see comments in `send_txs` below. for (auto& connection : connections) { std::sort(connection.first.begin(), connection.first.end()); // don't leak receive order - make_payload_send_txs(*zone_->p2p, std::move(connection.first), connection.second, zone_->pad_txs); + make_payload_send_txs(*zone_->p2p, std::move(connection.first), connection.second, zone_->pad_txs, zone_->is_public); } if (next_flush != std::chrono::steady_clock::time_point::max()) @@ -387,29 +383,38 @@ namespace levin void operator()() { - if (!zone_ || !zone_->p2p || txs_.empty()) + run(std::move(zone_), epee::to_span(txs_), source_); + } + + static void run(std::shared_ptr zone, epee::span txs, const boost::uuids::uuid& source) + { + if (!zone || !zone->p2p || txs.empty()) return; - assert(zone_->strand.running_in_this_thread()); + assert(zone->strand.running_in_this_thread()); const auto now = std::chrono::steady_clock::now(); auto next_flush = std::chrono::steady_clock::time_point::max(); - random_poisson in_duration(fluff_average_in); - random_poisson out_duration(fluff_average_out); + crypto::random_poisson_subseconds in_duration(fluff_average_in); + crypto::random_poisson_subseconds out_duration(fluff_average_out); + + + MDEBUG("Queueing " << txs.size() << " transaction(s) for Dandelion++ fluffing"); bool available = false; - zone_->p2p->foreach_connection([this, now, &in_duration, &out_duration, &next_flush, &available] (detail::p2p_context& context) + zone->p2p->foreach_connection([txs, now, &zone, &source, &in_duration, &out_duration, &next_flush, &available] (detail::p2p_context& context) { - if (this->source_ != context.m_connection_id && (this->zone_->is_public || !context.m_is_income)) + // When i2p/tor, only fluff to outbound connections + if (source != context.m_connection_id && (zone->is_public || !context.m_is_income)) { available = true; if (context.fluff_txs.empty()) context.flush_time = now + (context.m_is_income ? in_duration() : out_duration()); next_flush = std::min(next_flush, context.flush_time); - context.fluff_txs.reserve(context.fluff_txs.size() + this->txs_.size()); - for (const blobdata& tx : this->txs_) + context.fluff_txs.reserve(context.fluff_txs.size() + txs.size()); + for (const blobdata& tx : txs) context.fluff_txs.push_back(tx); // must copy instead of move (multiple conns) } return true; @@ -418,8 +423,8 @@ namespace levin if (!available) MWARNING("Unable to send transaction(s), no available connections"); - if (next_flush < zone_->flush_time) - fluff_flush::queue(std::move(zone_), next_flush); + if (next_flush < zone->flush_time) + fluff_flush::queue(std::move(zone), next_flush); } }; @@ -471,6 +476,11 @@ namespace levin assert(zone->strand.running_in_this_thread()); zone->connection_count = zone->map.size(); + + // only noise uses the "noise channels", only update when enabled + if (zone->noise.empty()) + return; + for (auto id = zone->map.begin(); id != zone->map.end(); ++id) { const std::size_t i = id - zone->map.begin(); @@ -478,27 +488,76 @@ namespace levin } } + //! \pre Called within `zone_->strand`. + static void run(std::shared_ptr zone, std::vector out_connections) + { + if (!zone) + return; + + assert(zone->strand.running_in_this_thread()); + if (zone->map.update(std::move(out_connections))) + post(std::move(zone)); + } + //! \pre Called within `zone_->strand`. void operator()() { - if (!zone_) + run(std::move(zone_), std::move(out_connections_)); + } + }; + + //! Checks fluff status for this node, and then does stem or fluff for txes + struct dandelionpp_notify + { + std::shared_ptr zone_; + i_core_events* core_; + std::vector txs_; + boost::uuids::uuid source_; + + //! \pre Called in `zone_->strand` + void operator()() + { + if (!zone_ || !core_ || txs_.empty()) return; - assert(zone_->strand.running_in_this_thread()); - if (zone_->map.update(std::move(out_connections_))) - post(std::move(zone_)); + if (zone_->fluffing) + { + core_->on_transactions_relayed(epee::to_span(txs_), relay_method::fluff); + fluff_notify::run(std::move(zone_), epee::to_span(txs_), source_); + } + else // forward tx in stem + { + core_->on_transactions_relayed(epee::to_span(txs_), relay_method::stem); + for (int tries = 2; 0 < tries; tries--) + { + const boost::uuids::uuid destination = zone_->map.get_stem(source_); + if (!destination.is_nil() && make_payload_send_txs(*zone_->p2p, std::vector{txs_}, destination, zone_->pad_txs, false)) + { + /* Source is intentionally omitted in debug log for privacy - a + nil uuid indicates source is that node. */ + MDEBUG("Sent " << txs_.size() << " transaction(s) to " << destination << " using Dandelion++ stem"); + return; + } + + // connection list may be outdated, try again + update_channels::run(zone_, get_out_connections(*zone_->p2p)); + } + + MERROR("Unable to send transaction(s) via Dandelion++ stem"); + } } }; - //! Swaps out noise channels entirely; new epoch start. + //! Swaps out noise/dandelionpp channels entirely; new epoch start. class change_channels { std::shared_ptr zone_; net::dandelionpp::connection_map map_; // Requires manual copy constructor + bool fluffing_; public: - explicit change_channels(std::shared_ptr zone, net::dandelionpp::connection_map map) - : zone_(std::move(zone)), map_(std::move(map)) + explicit change_channels(std::shared_ptr zone, net::dandelionpp::connection_map map, const bool fluffing) + : zone_(std::move(zone)), map_(std::move(map)), fluffing_(fluffing) {} change_channels(change_channels&&) = default; @@ -510,11 +569,15 @@ namespace levin void operator()() { if (!zone_) - return + return; assert(zone_->strand.running_in_this_thread()); + if (zone_->is_public) + MDEBUG("Starting new Dandelion++ epoch: " << (fluffing_ ? "fluff" : "stem")); + zone_->map = std::move(map_); + zone_->fluffing = fluffing_; update_channels::post(std::move(zone_)); } }; @@ -608,9 +671,10 @@ namespace levin if (error && error != boost::system::errc::operation_canceled) throw boost::system::system_error{error, "start_epoch timer failed"}; + const bool fluffing = crypto::rand_idx(unsigned(100)) < CRYPTONOTE_DANDELIONPP_FLUFF_PROBABILITY; const auto start = std::chrono::steady_clock::now(); zone_->strand.dispatch( - change_channels{zone_, net::dandelionpp::connection_map{get_out_connections(*(zone_->p2p)), count_}} + change_channels{zone_, net::dandelionpp::connection_map{get_out_connections(*(zone_->p2p)), count_}, fluffing} ); detail::zone& alias = *zone_; @@ -626,10 +690,16 @@ namespace levin if (!zone_->p2p) throw std::logic_error{"cryptonote::levin::notify cannot have nullptr p2p argument"}; - if (!zone_->noise.empty()) + const bool noise_enabled = !zone_->noise.empty(); + if (noise_enabled || is_public) { const auto now = std::chrono::steady_clock::now(); - start_epoch{zone_, noise_min_epoch, noise_epoch_range, CRYPTONOTE_NOISE_CHANNELS}(); + const auto min_epoch = noise_enabled ? noise_min_epoch : dandelionpp_min_epoch; + const auto epoch_range = noise_enabled ? noise_epoch_range : dandelionpp_epoch_range; + const std::size_t out_count = noise_enabled ? CRYPTONOTE_NOISE_CHANNELS : CRYPTONOTE_DANDELIONPP_STEMS; + + start_epoch{zone_, min_epoch, epoch_range, out_count}(); + for (std::size_t channel = 0; channel < zone_->channels.size(); ++channel) send_noise::wait(now, zone_, channel); } @@ -679,7 +749,7 @@ namespace levin zone_->flush_txs.cancel(); } - bool notify::send_txs(std::vector txs, const boost::uuids::uuid& source) + bool notify::send_txs(std::vector txs, const boost::uuids::uuid& source, i_core_events& core, relay_method tx_relay) { if (txs.empty()) return true; @@ -687,6 +757,17 @@ namespace levin if (!zone_) return false; + /* If noise is enabled in a zone, it always takes precedence. The technique + provides good protection against ISP adversaries, but not sybil + adversaries. Noise is currently only enabled over I2P/Tor - those + networks provide protection against sybil attacks (we only send to + outgoing connections). + + If noise is disabled, Dandelion++ is used for public networks only. + Dandelion++ over I2P/Tor should be an interesting case to investigate, + but the mempool/stempool needs to know the zone a tx originated from to + work properly. */ + if (!zone_->noise.empty() && !zone_->channels.empty()) { // covert send in "noise" channel @@ -694,8 +775,17 @@ namespace levin CRYPTONOTE_MAX_FRAGMENTS * CRYPTONOTE_NOISE_BYTES <= LEVIN_DEFAULT_MAX_PACKET_SIZE, "most nodes will reject this fragment setting" ); - // padding is not useful when using noise mode - const std::string payload = make_tx_payload(std::move(txs), false); + if (tx_relay == relay_method::stem) + { + MWARNING("Dandelion++ stem not supported over noise networks"); + tx_relay = relay_method::local; // do not put into stempool embargo (hopefully not there already!). + } + + core.on_transactions_relayed(epee::to_span(txs), tx_relay); + + // Padding is not useful when using noise mode. Send as stem so receiver + // forwards in Dandelion++ mode. + const std::string payload = make_tx_payload(std::move(txs), false, false); epee::byte_slice message = epee::levin::make_fragmented_notify( zone_->noise, NOTIFY_NEW_TRANSACTIONS::ID, epee::strspan(payload) ); @@ -714,9 +804,31 @@ namespace levin } else { - zone_->strand.dispatch(fluff_notify{zone_, std::move(txs), source}); + switch (tx_relay) + { + default: + case relay_method::none: + case relay_method::block: + return false; + case relay_method::stem: + tx_relay = relay_method::fluff; // don't set stempool embargo when skipping to fluff + /* fallthrough */ + case relay_method::local: + if (zone_->is_public) + { + // this will change a local tx to stem or fluff ... + zone_->strand.dispatch( + dandelionpp_notify{zone_, std::addressof(core), std::move(txs), source} + ); + break; + } + /* fallthrough */ + case relay_method::fluff: + core.on_transactions_relayed(epee::to_span(txs), tx_relay); + zone_->strand.dispatch(fluff_notify{zone_, std::move(txs), source}); + break; + } } - return true; } } // levin diff --git a/src/cryptonote_protocol/levin_notify.h b/src/cryptonote_protocol/levin_notify.h index ce652d933..641f1f956 100644 --- a/src/cryptonote_protocol/levin_notify.h +++ b/src/cryptonote_protocol/levin_notify.h @@ -35,6 +35,7 @@ #include "byte_slice.h" #include "cryptonote_basic/blobdatatype.h" +#include "cryptonote_protocol/enums.h" #include "cryptonote_protocol/fwd.h" #include "net/enums.h" #include "span.h" @@ -122,7 +123,7 @@ namespace levin particular stem. \return True iff the notification is queued for sending. */ - bool send_txs(std::vector txs, const boost::uuids::uuid& source); + bool send_txs(std::vector txs, const boost::uuids::uuid& source, i_core_events& core, relay_method tx_relay); }; } // levin } // net diff --git a/src/p2p/net_node.h b/src/p2p/net_node.h index 31d8aad3f..5337106dd 100644 --- a/src/p2p/net_node.h +++ b/src/p2p/net_node.h @@ -334,7 +334,7 @@ namespace nodetool virtual void callback(p2p_connection_context& context); //----------------- i_p2p_endpoint ------------------------------------------------------------- virtual bool relay_notify_to_list(int command, const epee::span data_buff, std::vector> connections); - virtual epee::net_utils::zone send_txs(std::vector txs, const epee::net_utils::zone origin, const boost::uuids::uuid& source, cryptonote::i_core_events& core); + virtual epee::net_utils::zone send_txs(std::vector txs, const epee::net_utils::zone origin, const boost::uuids::uuid& source, cryptonote::i_core_events& core, cryptonote::relay_method tx_relay); virtual bool invoke_command_to_peer(int command, const epee::span req_buff, std::string& resp_buff, const epee::net_utils::connection_context_base& context); virtual bool invoke_notify_to_peer(int command, const epee::span req_buff, const epee::net_utils::connection_context_base& context); virtual bool drop_connection(const epee::net_utils::connection_context_base& context); diff --git a/src/p2p/net_node.inl b/src/p2p/net_node.inl index 09058c76a..4c253b673 100644 --- a/src/p2p/net_node.inl +++ b/src/p2p/net_node.inl @@ -1975,18 +1975,13 @@ namespace nodetool } //----------------------------------------------------------------------------------- template - epee::net_utils::zone node_server::send_txs(std::vector txs, const epee::net_utils::zone origin, const boost::uuids::uuid& source, cryptonote::i_core_events& core) + epee::net_utils::zone node_server::send_txs(std::vector txs, const epee::net_utils::zone origin, const boost::uuids::uuid& source, cryptonote::i_core_events& core, const cryptonote::relay_method tx_relay) { namespace enet = epee::net_utils; - const auto send = [&txs, &source, &core] (std::pair& network) + const auto send = [&txs, &source, &core, tx_relay] (std::pair& network) { - const bool is_public = (network.first == enet::zone::public_); - const cryptonote::relay_method tx_relay = is_public ? - cryptonote::relay_method::fluff : cryptonote::relay_method::local; - - core.on_transactions_relayed(epee::to_span(txs), tx_relay); - if (network.second.m_notifier.send_txs(std::move(txs), source)) + if (network.second.m_notifier.send_txs(std::move(txs), source, core, tx_relay)) return network.first; return enet::zone::invalid; }; diff --git a/src/p2p/net_node_common.h b/src/p2p/net_node_common.h index ed88aa28c..6a6100e0c 100644 --- a/src/p2p/net_node_common.h +++ b/src/p2p/net_node_common.h @@ -50,7 +50,7 @@ namespace nodetool struct i_p2p_endpoint { virtual bool relay_notify_to_list(int command, const epee::span data_buff, std::vector> connections)=0; - virtual epee::net_utils::zone send_txs(std::vector txs, const epee::net_utils::zone origin, const boost::uuids::uuid& source, cryptonote::i_core_events& core)=0; + virtual epee::net_utils::zone send_txs(std::vector txs, const epee::net_utils::zone origin, const boost::uuids::uuid& source, cryptonote::i_core_events& core, cryptonote::relay_method tx_relay)=0; virtual bool invoke_command_to_peer(int command, const epee::span req_buff, std::string& resp_buff, const epee::net_utils::connection_context_base& context)=0; virtual bool invoke_notify_to_peer(int command, const epee::span req_buff, const epee::net_utils::connection_context_base& context)=0; virtual bool drop_connection(const epee::net_utils::connection_context_base& context)=0; @@ -75,7 +75,7 @@ namespace nodetool { return false; } - virtual epee::net_utils::zone send_txs(std::vector txs, const epee::net_utils::zone origin, const boost::uuids::uuid& source, cryptonote::i_core_events& core) + virtual epee::net_utils::zone send_txs(std::vector txs, const epee::net_utils::zone origin, const boost::uuids::uuid& source, cryptonote::i_core_events& core, cryptonote::relay_method tx_relay) { return epee::net_utils::zone::invalid; } diff --git a/src/rpc/core_rpc_server.cpp b/src/rpc/core_rpc_server.cpp index f097c93fa..1fd0c037b 100644 --- a/src/rpc/core_rpc_server.cpp +++ b/src/rpc/core_rpc_server.cpp @@ -1152,7 +1152,7 @@ namespace cryptonote return true; } - if(!tvc.m_should_be_relayed) + if(tvc.m_relay == relay_method::none) { LOG_PRINT_L0("[on_send_raw_tx]: tx accepted, but not relayed"); res.reason = "Not relayed"; @@ -1162,8 +1162,8 @@ namespace cryptonote } NOTIFY_NEW_TRANSACTIONS::request r; - r.txs.push_back(tx_blob); - m_core.get_protocol()->relay_transactions(r, boost::uuids::nil_uuid(), epee::net_utils::zone::invalid); + r.txs.push_back(std::move(tx_blob)); + m_core.get_protocol()->relay_transactions(r, boost::uuids::nil_uuid(), epee::net_utils::zone::invalid, relay_method::local); //TODO: make sure that tx has reached other nodes here, probably wait to receive reflections from other nodes res.status = CORE_RPC_STATUS_OK; return true; @@ -2776,8 +2776,8 @@ namespace cryptonote if (!m_core.get_pool_transaction(txid, txblob, relay_category::legacy)) { NOTIFY_NEW_TRANSACTIONS::request r; - r.txs.push_back(txblob); - m_core.get_protocol()->relay_transactions(r, boost::uuids::nil_uuid(), epee::net_utils::zone::invalid); + r.txs.push_back(std::move(txblob)); + m_core.get_protocol()->relay_transactions(r, boost::uuids::nil_uuid(), epee::net_utils::zone::invalid, relay_method::local); //TODO: make sure that tx has reached other nodes here, probably wait to receive reflections from other nodes } else diff --git a/src/rpc/daemon_handler.cpp b/src/rpc/daemon_handler.cpp index 7292176b4..d05854e34 100644 --- a/src/rpc/daemon_handler.cpp +++ b/src/rpc/daemon_handler.cpp @@ -349,10 +349,10 @@ namespace rpc res.error_details = "Invalid hex"; return; } - handleTxBlob(tx_blob, req.relay, res); + handleTxBlob(std::move(tx_blob), req.relay, res); } - void DaemonHandler::handleTxBlob(const std::string& tx_blob, bool relay, SendRawTx::Response& res) + void DaemonHandler::handleTxBlob(std::string&& tx_blob, bool relay, SendRawTx::Response& res) { if (!m_p2p.get_payload_object().is_synchronized()) { @@ -423,7 +423,7 @@ namespace rpc return; } - if(!tvc.m_should_be_relayed || !relay) + if(tvc.m_relay == relay_method::none || !relay) { MERROR("[SendRawTx]: tx accepted, but not relayed"); res.error_details = "Not relayed"; @@ -434,8 +434,8 @@ namespace rpc } NOTIFY_NEW_TRANSACTIONS::request r; - r.txs.push_back(tx_blob); - m_core.get_protocol()->relay_transactions(r, boost::uuids::nil_uuid(), epee::net_utils::zone::invalid); + r.txs.push_back(std::move(tx_blob)); + m_core.get_protocol()->relay_transactions(r, boost::uuids::nil_uuid(), epee::net_utils::zone::invalid, relay_method::local); //TODO: make sure that tx has reached other nodes here, probably wait to receive reflections from other nodes res.status = Message::STATUS_OK; diff --git a/src/rpc/daemon_handler.h b/src/rpc/daemon_handler.h index c33f608ab..61eac17f0 100644 --- a/src/rpc/daemon_handler.h +++ b/src/rpc/daemon_handler.h @@ -138,7 +138,7 @@ class DaemonHandler : public RpcHandler bool getBlockHeaderByHash(const crypto::hash& hash_in, cryptonote::rpc::BlockHeaderResponse& response); - void handleTxBlob(const std::string& tx_blob, bool relay, SendRawTx::Response& res); + void handleTxBlob(std::string&& tx_blob, bool relay, SendRawTx::Response& res); cryptonote::core& m_core; t_p2p& m_p2p; diff --git a/tests/core_tests/chaingen.h b/tests/core_tests/chaingen.h index 80ce7404b..453ee923d 100644 --- a/tests/core_tests/chaingen.h +++ b/tests/core_tests/chaingen.h @@ -116,7 +116,8 @@ struct event_visitor_settings { set_txs_keeped_by_block = 1 << 0, set_txs_do_not_relay = 1 << 1, - set_local_relay = 1 << 2 + set_local_relay = 1 << 2, + set_txs_stem = 1 << 3 }; event_visitor_settings(int a_mask = 0) @@ -548,6 +549,10 @@ public: { m_tx_relay = cryptonote::relay_method::none; } + else if (settings.mask & event_visitor_settings::set_txs_stem) + { + m_tx_relay = cryptonote::relay_method::stem; + } else { m_tx_relay = cryptonote::relay_method::fluff; diff --git a/tests/core_tests/chaingen_main.cpp b/tests/core_tests/chaingen_main.cpp index 014c7475b..8a313140c 100644 --- a/tests/core_tests/chaingen_main.cpp +++ b/tests/core_tests/chaingen_main.cpp @@ -162,6 +162,7 @@ int main(int argc, char* argv[]) GENERATE_AND_PLAY(txpool_double_spend_norelay); GENERATE_AND_PLAY(txpool_double_spend_local); GENERATE_AND_PLAY(txpool_double_spend_keyimage); + GENERATE_AND_PLAY(txpool_stem_loop); // Double spend GENERATE_AND_PLAY(gen_double_spend_in_tx); diff --git a/tests/core_tests/tx_pool.cpp b/tests/core_tests/tx_pool.cpp index cc738c4ba..b60dd00fc 100644 --- a/tests/core_tests/tx_pool.cpp +++ b/tests/core_tests/tx_pool.cpp @@ -552,7 +552,6 @@ bool txpool_double_spend_local::generate(std::vector& events) DO_CALLBACK(events, "mark_no_new"); events.push_back(tx_0); DO_CALLBACK(events, "check_txpool_spent_keys"); - DO_CALLBACK(events, "mark_timestamp_change"); DO_CALLBACK(events, "check_unchanged"); SET_EVENT_VISITOR_SETT(events, 0); DO_CALLBACK(events, "timestamp_change_pause"); @@ -580,6 +579,7 @@ bool txpool_double_spend_keyimage::generate(std::vector& event const std::size_t tx_index1 = events.size(); MAKE_TX(events, tx_0, miner_account, bob_account, send_amount, blk_0); + SET_EVENT_VISITOR_SETT(events, event_visitor_settings::set_txs_stem); DO_CALLBACK(events, "increase_all_tx_count"); DO_CALLBACK(events, "check_txpool_spent_keys"); DO_CALLBACK(events, "mark_timestamp_change"); @@ -611,3 +611,30 @@ bool txpool_double_spend_keyimage::generate(std::vector& event return true; } +bool txpool_stem_loop::generate(std::vector& events) const +{ + INIT_MEMPOOL_TEST(); + + DO_CALLBACK(events, "check_txpool_spent_keys"); + SET_EVENT_VISITOR_SETT(events, event_visitor_settings::set_txs_stem); + DO_CALLBACK(events, "mark_no_new"); + + MAKE_TX(events, tx_0, miner_account, bob_account, send_amount, blk_0); + + DO_CALLBACK(events, "increase_all_tx_count"); + DO_CALLBACK(events, "check_txpool_spent_keys"); + DO_CALLBACK(events, "mark_timestamp_change"); + DO_CALLBACK(events, "check_new_hidden"); + DO_CALLBACK(events, "timestamp_change_pause"); + events.push_back(tx_0); + DO_CALLBACK(events, "increase_broadcasted_tx_count"); + DO_CALLBACK(events, "check_txpool_spent_keys"); + DO_CALLBACK(events, "mark_timestamp_change"); + DO_CALLBACK(events, "check_new_broadcasted"); + DO_CALLBACK(events, "timestamp_change_pause"); + DO_CALLBACK(events, "mark_no_new"); + events.push_back(tx_0); + DO_CALLBACK(events, "check_unchanged"); + + return true; +} diff --git a/tests/core_tests/tx_pool.h b/tests/core_tests/tx_pool.h index eb71dcf79..8a27e98f0 100644 --- a/tests/core_tests/tx_pool.h +++ b/tests/core_tests/tx_pool.h @@ -127,3 +127,12 @@ struct txpool_double_spend_keyimage : txpool_double_spend_base bool generate(std::vector& events) const; }; + +struct txpool_stem_loop : txpool_double_spend_base +{ + txpool_stem_loop() + : txpool_double_spend_base() + {} + + bool generate(std::vector& events) const; +}; diff --git a/tests/unit_tests/levin.cpp b/tests/unit_tests/levin.cpp index 720103e5a..62b101a42 100644 --- a/tests/unit_tests/levin.cpp +++ b/tests/unit_tests/levin.cpp @@ -34,11 +34,14 @@ #include #include #include +#include #include "byte_slice.h" #include "crypto/crypto.h" #include "cryptonote_basic/connection_context.h" +#include "cryptonote_config.h" #include "cryptonote_core/cryptonote_core.h" +#include "cryptonote_core/i_core_events.h" #include "cryptonote_protocol/cryptonote_protocol_defs.h" #include "cryptonote_protocol/levin_notify.h" #include "int-util.h" @@ -113,6 +116,44 @@ namespace std::deque send_queue_; }; + class test_core_events final : public cryptonote::i_core_events + { + std::map> relayed_; + + virtual void on_transactions_relayed(epee::span txes, cryptonote::relay_method relay) override final + { + std::vector& cached = relayed_[relay]; + for (const auto& tx : txes) + cached.push_back(tx); + } + + public: + test_core_events() + : relayed_() + {} + + std::size_t relayed_method_size() const noexcept + { + return relayed_.size(); + } + + bool has_stem_txes() const noexcept + { + return relayed_.count(cryptonote::relay_method::stem); + } + + std::vector take_relayed(cryptonote::relay_method relay) + { + auto elems = relayed_.find(relay); + if (elems == relayed_.end()) + throw std::logic_error{"on_transactions_relayed empty"}; + + std::vector out{std::move(elems->second)}; + relayed_.erase(elems); + return out; + } + }; + class test_connection { test_endpoint endpoint_; @@ -146,6 +187,11 @@ namespace { return context_.m_connection_id; } + + bool is_incoming() const noexcept + { + return context_.m_is_income; + } }; struct received_message @@ -155,7 +201,7 @@ namespace std::string payload; }; - class test_receiver : public epee::levin::levin_commands_handler + class test_receiver final : public epee::levin::levin_commands_handler { std::deque invoked_; std::deque notified_; @@ -253,7 +299,8 @@ namespace random_generator_(), io_service_(), receiver_(), - contexts_() + contexts_(), + events_() { connections_->set_handler(std::addressof(receiver_), nullptr); } @@ -262,6 +309,7 @@ namespace { EXPECT_EQ(0u, receiver_.invoked_size()); EXPECT_EQ(0u, receiver_.notified_size()); + EXPECT_EQ(0u, events_.relayed_method_size()); } void add_connection(const bool is_incoming) @@ -283,6 +331,7 @@ namespace boost::asio::io_service io_service_; test_receiver receiver_; std::deque contexts_; + test_core_events events_; }; } @@ -434,11 +483,11 @@ TEST_F(levin_notify, defaulted) EXPECT_FALSE(status.has_noise); EXPECT_FALSE(status.connections_filled); } - EXPECT_TRUE(notifier.send_txs({}, random_generator_())); + EXPECT_TRUE(notifier.send_txs({}, random_generator_(), events_, cryptonote::relay_method::local)); std::vector txs(2); txs[0].resize(100, 'e'); - EXPECT_FALSE(notifier.send_txs(std::move(txs), random_generator_())); + EXPECT_FALSE(notifier.send_txs(std::move(txs), random_generator_(), events_, cryptonote::relay_method::local)); } TEST_F(levin_notify, fluff_without_padding) @@ -455,11 +504,6 @@ TEST_F(levin_notify, fluff_without_padding) } notifier.new_out_connection(); io_service_.poll(); - { - const auto status = notifier.get_status(); - EXPECT_FALSE(status.has_noise); - EXPECT_FALSE(status.connections_filled); // not tracked - } std::vector txs(2); txs[0].resize(100, 'f'); @@ -468,7 +512,7 @@ TEST_F(levin_notify, fluff_without_padding) ASSERT_EQ(10u, contexts_.size()); { auto context = contexts_.begin(); - EXPECT_TRUE(notifier.send_txs(txs, context->get_id())); + EXPECT_TRUE(notifier.send_txs(txs, context->get_id(), events_, cryptonote::relay_method::fluff)); io_service_.reset(); ASSERT_LT(0u, io_service_.poll()); @@ -479,6 +523,7 @@ TEST_F(levin_notify, fluff_without_padding) for (++context; context != contexts_.end(); ++context) EXPECT_EQ(1u, context->process_send_queue()); + EXPECT_EQ(txs, events_.take_relayed(cryptonote::relay_method::fluff)); std::sort(txs.begin(), txs.end()); ASSERT_EQ(9u, receiver_.notified_size()); for (unsigned count = 0; count < 9; ++count) @@ -486,13 +531,14 @@ TEST_F(levin_notify, fluff_without_padding) auto notification = receiver_.get_notification().second; EXPECT_EQ(txs, notification.txs); EXPECT_TRUE(notification._.empty()); + EXPECT_TRUE(notification.dandelionpp_fluff); } } } -TEST_F(levin_notify, fluff_with_padding) +TEST_F(levin_notify, stem_without_padding) { - cryptonote::levin::notify notifier = make_notifier(0, true, true); + cryptonote::levin::notify notifier = make_notifier(0, true, false); for (unsigned count = 0; count < 10; ++count) add_connection(count % 2 == 0); @@ -504,44 +550,65 @@ TEST_F(levin_notify, fluff_with_padding) } notifier.new_out_connection(); io_service_.poll(); - { - const auto status = notifier.get_status(); - EXPECT_FALSE(status.has_noise); - EXPECT_FALSE(status.connections_filled); // not tracked - } std::vector txs(2); txs[0].resize(100, 'f'); txs[1].resize(200, 'e'); + std::vector sorted_txs = txs; + std::sort(sorted_txs.begin(), sorted_txs.end()); + ASSERT_EQ(10u, contexts_.size()); + bool has_stemmed = false; + bool has_fluffed = false; + while (!has_stemmed || !has_fluffed) { auto context = contexts_.begin(); - EXPECT_TRUE(notifier.send_txs(txs, context->get_id())); + EXPECT_TRUE(notifier.send_txs(txs, context->get_id(), events_, cryptonote::relay_method::stem)); io_service_.reset(); ASSERT_LT(0u, io_service_.poll()); - notifier.run_fluff(); - ASSERT_LT(0u, io_service_.poll()); + const bool is_stem = events_.has_stem_txes(); + EXPECT_EQ(txs, events_.take_relayed(is_stem ? cryptonote::relay_method::stem : cryptonote::relay_method::fluff)); + if (!is_stem) + { + notifier.run_fluff(); + ASSERT_LT(0u, io_service_.poll()); + } + + std::size_t send_count = 0; EXPECT_EQ(0u, context->process_send_queue()); for (++context; context != contexts_.end(); ++context) - EXPECT_EQ(1u, context->process_send_queue()); + { + const std::size_t sent = context->process_send_queue(); + if (sent && is_stem) + EXPECT_EQ(1u, (context - contexts_.begin()) % 2); + send_count += sent; + } - std::sort(txs.begin(), txs.end()); - ASSERT_EQ(9u, receiver_.notified_size()); - for (unsigned count = 0; count < 9; ++count) + EXPECT_EQ(is_stem ? 1u : 9u, send_count); + ASSERT_EQ(is_stem ? 1u : 9u, receiver_.notified_size()); + for (unsigned count = 0; count < (is_stem ? 1u : 9u); ++count) { auto notification = receiver_.get_notification().second; - EXPECT_EQ(txs, notification.txs); - EXPECT_FALSE(notification._.empty()); + if (is_stem) + EXPECT_EQ(txs, notification.txs); + else + EXPECT_EQ(sorted_txs, notification.txs); + EXPECT_TRUE(notification._.empty()); + EXPECT_EQ(!is_stem, notification.dandelionpp_fluff); } + + has_stemmed |= is_stem; + has_fluffed |= !is_stem; + notifier.run_epoch(); } } -TEST_F(levin_notify, private_fluff_without_padding) +TEST_F(levin_notify, local_without_padding) { - cryptonote::levin::notify notifier = make_notifier(0, false, false); + cryptonote::levin::notify notifier = make_notifier(0, true, false); for (unsigned count = 0; count < 10; ++count) add_connection(count % 2 == 0); @@ -553,48 +620,94 @@ TEST_F(levin_notify, private_fluff_without_padding) } notifier.new_out_connection(); io_service_.poll(); - { - const auto status = notifier.get_status(); - EXPECT_FALSE(status.has_noise); - EXPECT_FALSE(status.connections_filled); // not tracked - } std::vector txs(2); txs[0].resize(100, 'f'); txs[1].resize(200, 'e'); + std::vector sorted_txs = txs; + std::sort(sorted_txs.begin(), sorted_txs.end()); + ASSERT_EQ(10u, contexts_.size()); + bool has_stemmed = false; + bool has_fluffed = false; + while (!has_stemmed || !has_fluffed) { auto context = contexts_.begin(); - EXPECT_TRUE(notifier.send_txs(txs, context->get_id())); + EXPECT_TRUE(notifier.send_txs(txs, context->get_id(), events_, cryptonote::relay_method::local)); io_service_.reset(); ASSERT_LT(0u, io_service_.poll()); - notifier.run_fluff(); - io_service_.reset(); - ASSERT_LT(0u, io_service_.poll()); + const bool is_stem = events_.has_stem_txes(); + EXPECT_EQ(txs, events_.take_relayed(is_stem ? cryptonote::relay_method::stem : cryptonote::relay_method::fluff)); - std::sort(txs.begin(), txs.end()); + if (!is_stem) + { + notifier.run_fluff(); + ASSERT_LT(0u, io_service_.poll()); + } + + std::size_t send_count = 0; EXPECT_EQ(0u, context->process_send_queue()); for (++context; context != contexts_.end(); ++context) { - const bool is_incoming = ((context - contexts_.begin()) % 2 == 0); - EXPECT_EQ(is_incoming ? 0u : 1u, context->process_send_queue()); + const std::size_t sent = context->process_send_queue(); + if (sent && is_stem) + EXPECT_EQ(1u, (context - contexts_.begin()) % 2); + send_count += sent; } - ASSERT_EQ(5u, receiver_.notified_size()); - for (unsigned count = 0; count < 5; ++count) + EXPECT_EQ(is_stem ? 1u : 9u, send_count); + ASSERT_EQ(is_stem ? 1u : 9u, receiver_.notified_size()); + for (unsigned count = 0; count < (is_stem ? 1u : 9u); ++count) { auto notification = receiver_.get_notification().second; - EXPECT_EQ(txs, notification.txs); + if (is_stem) + EXPECT_EQ(txs, notification.txs); + else + EXPECT_EQ(sorted_txs, notification.txs); EXPECT_TRUE(notification._.empty()); + EXPECT_EQ(!is_stem, notification.dandelionpp_fluff); } + + has_stemmed |= is_stem; + has_fluffed |= !is_stem; + notifier.run_epoch(); } } -TEST_F(levin_notify, private_fluff_with_padding) +TEST_F(levin_notify, block_without_padding) { - cryptonote::levin::notify notifier = make_notifier(0, false, true); + cryptonote::levin::notify notifier = make_notifier(0, true, false); + + for (unsigned count = 0; count < 10; ++count) + add_connection(count % 2 == 0); + + { + const auto status = notifier.get_status(); + EXPECT_FALSE(status.has_noise); + EXPECT_FALSE(status.connections_filled); + } + notifier.new_out_connection(); + io_service_.poll(); + + std::vector txs(2); + txs[0].resize(100, 'e'); + txs[1].resize(200, 'f'); + + ASSERT_EQ(10u, contexts_.size()); + { + auto context = contexts_.begin(); + EXPECT_FALSE(notifier.send_txs(txs, context->get_id(), events_, cryptonote::relay_method::block)); + + io_service_.reset(); + ASSERT_EQ(0u, io_service_.poll()); + } +} + +TEST_F(levin_notify, none_without_padding) +{ + cryptonote::levin::notify notifier = make_notifier(0, true, false); for (unsigned count = 0; count < 10; ++count) add_connection(count % 2 == 0); @@ -606,11 +719,35 @@ TEST_F(levin_notify, private_fluff_with_padding) } notifier.new_out_connection(); io_service_.poll(); + + std::vector txs(2); + txs[0].resize(100, 'e'); + txs[1].resize(200, 'f'); + + ASSERT_EQ(10u, contexts_.size()); + { + auto context = contexts_.begin(); + EXPECT_FALSE(notifier.send_txs(txs, context->get_id(), events_, cryptonote::relay_method::none)); + + io_service_.reset(); + ASSERT_EQ(0u, io_service_.poll()); + } +} + +TEST_F(levin_notify, fluff_with_padding) +{ + cryptonote::levin::notify notifier = make_notifier(0, true, true); + + for (unsigned count = 0; count < 10; ++count) + add_connection(count % 2 == 0); + { const auto status = notifier.get_status(); EXPECT_FALSE(status.has_noise); - EXPECT_FALSE(status.connections_filled); // not tracked + EXPECT_FALSE(status.connections_filled); } + notifier.new_out_connection(); + io_service_.poll(); std::vector txs(2); txs[0].resize(100, 'f'); @@ -619,102 +756,1011 @@ TEST_F(levin_notify, private_fluff_with_padding) ASSERT_EQ(10u, contexts_.size()); { auto context = contexts_.begin(); - EXPECT_TRUE(notifier.send_txs(txs, context->get_id())); + EXPECT_TRUE(notifier.send_txs(txs, context->get_id(), events_, cryptonote::relay_method::fluff)); io_service_.reset(); ASSERT_LT(0u, io_service_.poll()); notifier.run_fluff(); - io_service_.reset(); ASSERT_LT(0u, io_service_.poll()); + EXPECT_EQ(txs, events_.take_relayed(cryptonote::relay_method::fluff)); std::sort(txs.begin(), txs.end()); EXPECT_EQ(0u, context->process_send_queue()); for (++context; context != contexts_.end(); ++context) - { - const bool is_incoming = ((context - contexts_.begin()) % 2 == 0); - EXPECT_EQ(is_incoming ? 0u : 1u, context->process_send_queue()); - } + EXPECT_EQ(1u, context->process_send_queue()); - ASSERT_EQ(5u, receiver_.notified_size()); - for (unsigned count = 0; count < 5; ++count) + ASSERT_EQ(9u, receiver_.notified_size()); + for (unsigned count = 0; count < 9; ++count) { auto notification = receiver_.get_notification().second; EXPECT_EQ(txs, notification.txs); EXPECT_FALSE(notification._.empty()); + EXPECT_TRUE(notification.dandelionpp_fluff); } } } -TEST_F(levin_notify, noise) +TEST_F(levin_notify, stem_with_padding) { + cryptonote::levin::notify notifier = make_notifier(0, true, true); + for (unsigned count = 0; count < 10; ++count) add_connection(count % 2 == 0); - std::vector txs(1); - txs[0].resize(1900, 'h'); - - const boost::uuids::uuid incoming_id = random_generator_(); - cryptonote::levin::notify notifier = make_notifier(2048, false, true); - { const auto status = notifier.get_status(); - EXPECT_TRUE(status.has_noise); + EXPECT_FALSE(status.has_noise); EXPECT_FALSE(status.connections_filled); } - ASSERT_LT(0u, io_service_.poll()); + notifier.new_out_connection(); + io_service_.poll(); + + std::vector txs(2); + txs[0].resize(100, 'e'); + txs[1].resize(200, 'f'); + + ASSERT_EQ(10u, contexts_.size()); + bool has_stemmed = false; + bool has_fluffed = false; + while (!has_stemmed || !has_fluffed) { - const auto status = notifier.get_status(); - EXPECT_TRUE(status.has_noise); - EXPECT_TRUE(status.connections_filled); + auto context = contexts_.begin(); + EXPECT_TRUE(notifier.send_txs(txs, context->get_id(), events_, cryptonote::relay_method::stem)); + + io_service_.reset(); + ASSERT_LT(0u, io_service_.poll()); + const bool is_stem = events_.has_stem_txes(); + EXPECT_EQ(txs, events_.take_relayed(is_stem ? cryptonote::relay_method::stem : cryptonote::relay_method::fluff)); + + if (!is_stem) + { + notifier.run_fluff(); + ASSERT_LT(0u, io_service_.poll()); + } + + std::size_t send_count = 0; + EXPECT_EQ(0u, context->process_send_queue()); + for (++context; context != contexts_.end(); ++context) + { + const std::size_t sent = context->process_send_queue(); + if (sent && is_stem) + { + EXPECT_EQ(1u, (context - contexts_.begin()) % 2); + EXPECT_FALSE(context->is_incoming()); + } + send_count += sent; + } + + EXPECT_EQ(is_stem ? 1u : 9u, send_count); + ASSERT_EQ(is_stem ? 1u : 9u, receiver_.notified_size()); + for (unsigned count = 0; count < (is_stem ? 1u : 9u); ++count) + { + auto notification = receiver_.get_notification().second; + EXPECT_EQ(txs, notification.txs); + EXPECT_FALSE(notification._.empty()); + EXPECT_EQ(!is_stem, notification.dandelionpp_fluff); + } + + has_stemmed |= is_stem; + has_fluffed |= !is_stem; + notifier.run_epoch(); } +} - notifier.run_stems(); - io_service_.reset(); - ASSERT_LT(0u, io_service_.poll()); - { - std::size_t sent = 0; - for (auto& context : contexts_) - sent += context.process_send_queue(); +TEST_F(levin_notify, local_with_padding) +{ + cryptonote::levin::notify notifier = make_notifier(0, true, true); - EXPECT_EQ(2u, sent); - EXPECT_EQ(0u, receiver_.notified_size()); + for (unsigned count = 0; count < 10; ++count) + add_connection(count % 2 == 0); + + { + const auto status = notifier.get_status(); + EXPECT_FALSE(status.has_noise); + EXPECT_FALSE(status.connections_filled); } + notifier.new_out_connection(); + io_service_.poll(); - EXPECT_TRUE(notifier.send_txs(txs, incoming_id)); - notifier.run_stems(); - io_service_.reset(); - ASSERT_LT(0u, io_service_.poll()); + std::vector txs(2); + txs[0].resize(100, 'e'); + txs[1].resize(200, 'f'); + + ASSERT_EQ(10u, contexts_.size()); + bool has_stemmed = false; + bool has_fluffed = false; + while (!has_stemmed || !has_fluffed) { - std::size_t sent = 0; - for (auto& context : contexts_) - sent += context.process_send_queue(); + auto context = contexts_.begin(); + EXPECT_TRUE(notifier.send_txs(txs, context->get_id(), events_, cryptonote::relay_method::local)); - ASSERT_EQ(2u, sent); - while (sent--) + io_service_.reset(); + ASSERT_LT(0u, io_service_.poll()); + const bool is_stem = events_.has_stem_txes(); + EXPECT_EQ(txs, events_.take_relayed(is_stem ? cryptonote::relay_method::stem : cryptonote::relay_method::fluff)); + + if (!is_stem) + { + notifier.run_fluff(); + ASSERT_LT(0u, io_service_.poll()); + } + + std::size_t send_count = 0; + EXPECT_EQ(0u, context->process_send_queue()); + for (++context; context != contexts_.end(); ++context) + { + const std::size_t sent = context->process_send_queue(); + if (sent && is_stem) + { + EXPECT_EQ(1u, (context - contexts_.begin()) % 2); + EXPECT_FALSE(context->is_incoming()); + } + send_count += sent; + } + + EXPECT_EQ(is_stem ? 1u : 9u, send_count); + ASSERT_EQ(is_stem ? 1u : 9u, receiver_.notified_size()); + for (unsigned count = 0; count < (is_stem ? 1u : 9u); ++count) { auto notification = receiver_.get_notification().second; EXPECT_EQ(txs, notification.txs); - EXPECT_TRUE(notification._.empty()); + EXPECT_FALSE(notification._.empty()); + EXPECT_EQ(!is_stem, notification.dandelionpp_fluff); } + + has_stemmed |= is_stem; + has_fluffed |= !is_stem; + notifier.run_epoch(); + } +} + +TEST_F(levin_notify, block_with_padding) +{ + cryptonote::levin::notify notifier = make_notifier(0, true, true); + + for (unsigned count = 0; count < 10; ++count) + add_connection(count % 2 == 0); + + { + const auto status = notifier.get_status(); + EXPECT_FALSE(status.has_noise); + EXPECT_FALSE(status.connections_filled); } + notifier.new_out_connection(); + io_service_.poll(); - txs[0].resize(3000, 'r'); - EXPECT_TRUE(notifier.send_txs(txs, incoming_id)); - notifier.run_stems(); - io_service_.reset(); - ASSERT_LT(0u, io_service_.poll()); + std::vector txs(2); + txs[0].resize(100, 'e'); + txs[1].resize(200, 'f'); + + ASSERT_EQ(10u, contexts_.size()); { - std::size_t sent = 0; - for (auto& context : contexts_) - sent += context.process_send_queue(); + auto context = contexts_.begin(); + EXPECT_FALSE(notifier.send_txs(txs, context->get_id(), events_, cryptonote::relay_method::block)); - EXPECT_EQ(2u, sent); - EXPECT_EQ(0u, receiver_.notified_size()); + io_service_.reset(); + ASSERT_EQ(0u, io_service_.poll()); } +} - notifier.run_stems(); - io_service_.reset(); +TEST_F(levin_notify, none_with_padding) +{ + cryptonote::levin::notify notifier = make_notifier(0, true, true); + + for (unsigned count = 0; count < 10; ++count) + add_connection(count % 2 == 0); + + { + const auto status = notifier.get_status(); + EXPECT_FALSE(status.has_noise); + EXPECT_FALSE(status.connections_filled); + } + notifier.new_out_connection(); + io_service_.poll(); + + std::vector txs(2); + txs[0].resize(100, 'e'); + txs[1].resize(200, 'f'); + + ASSERT_EQ(10u, contexts_.size()); + { + auto context = contexts_.begin(); + EXPECT_FALSE(notifier.send_txs(txs, context->get_id(), events_, cryptonote::relay_method::none)); + + io_service_.reset(); + ASSERT_EQ(0u, io_service_.poll()); + } +} + +TEST_F(levin_notify, private_fluff_without_padding) +{ + cryptonote::levin::notify notifier = make_notifier(0, false, false); + + for (unsigned count = 0; count < 10; ++count) + add_connection(count % 2 == 0); + + { + const auto status = notifier.get_status(); + EXPECT_FALSE(status.has_noise); + EXPECT_FALSE(status.connections_filled); + } + notifier.new_out_connection(); + io_service_.poll(); + + std::vector txs(2); + txs[0].resize(100, 'e'); + txs[1].resize(200, 'f'); + + ASSERT_EQ(10u, contexts_.size()); + { + auto context = contexts_.begin(); + EXPECT_TRUE(notifier.send_txs(txs, context->get_id(), events_, cryptonote::relay_method::fluff)); + + io_service_.reset(); + ASSERT_LT(0u, io_service_.poll()); + notifier.run_fluff(); + io_service_.reset(); + ASSERT_LT(0u, io_service_.poll()); + + EXPECT_EQ(txs, events_.take_relayed(cryptonote::relay_method::fluff)); + + EXPECT_EQ(0u, context->process_send_queue()); + for (++context; context != contexts_.end(); ++context) + { + const bool is_incoming = ((context - contexts_.begin()) % 2 == 0); + EXPECT_EQ(is_incoming ? 0u : 1u, context->process_send_queue()); + } + + ASSERT_EQ(5u, receiver_.notified_size()); + for (unsigned count = 0; count < 5; ++count) + { + auto notification = receiver_.get_notification().second; + EXPECT_EQ(txs, notification.txs); + EXPECT_TRUE(notification._.empty()); + EXPECT_FALSE(notification.dandelionpp_fluff); + } + } +} + +TEST_F(levin_notify, private_stem_without_padding) +{ + // private mode always uses fluff but marked as stem + cryptonote::levin::notify notifier = make_notifier(0, false, false); + + for (unsigned count = 0; count < 10; ++count) + add_connection(count % 2 == 0); + + { + const auto status = notifier.get_status(); + EXPECT_FALSE(status.has_noise); + EXPECT_FALSE(status.connections_filled); + } + notifier.new_out_connection(); + io_service_.poll(); + + std::vector txs(2); + txs[0].resize(100, 'e'); + txs[1].resize(200, 'f'); + + ASSERT_EQ(10u, contexts_.size()); + { + auto context = contexts_.begin(); + EXPECT_TRUE(notifier.send_txs(txs, context->get_id(), events_, cryptonote::relay_method::stem)); + + io_service_.reset(); + ASSERT_LT(0u, io_service_.poll()); + notifier.run_fluff(); + io_service_.reset(); + ASSERT_LT(0u, io_service_.poll()); + + EXPECT_EQ(txs, events_.take_relayed(cryptonote::relay_method::fluff)); + + EXPECT_EQ(0u, context->process_send_queue()); + for (++context; context != contexts_.end(); ++context) + { + const bool is_incoming = ((context - contexts_.begin()) % 2 == 0); + EXPECT_EQ(is_incoming ? 0u : 1u, context->process_send_queue()); + } + + ASSERT_EQ(5u, receiver_.notified_size()); + for (unsigned count = 0; count < 5; ++count) + { + auto notification = receiver_.get_notification().second; + EXPECT_EQ(txs, notification.txs); + EXPECT_TRUE(notification._.empty()); + EXPECT_FALSE(notification.dandelionpp_fluff); + } + } +} + +TEST_F(levin_notify, private_local_without_padding) +{ + // private mode always uses fluff but marked as stem + cryptonote::levin::notify notifier = make_notifier(0, false, false); + + for (unsigned count = 0; count < 10; ++count) + add_connection(count % 2 == 0); + + { + const auto status = notifier.get_status(); + EXPECT_FALSE(status.has_noise); + EXPECT_FALSE(status.connections_filled); + } + notifier.new_out_connection(); + io_service_.poll(); + + std::vector txs(2); + txs[0].resize(100, 'e'); + txs[1].resize(200, 'f'); + + ASSERT_EQ(10u, contexts_.size()); + { + auto context = contexts_.begin(); + EXPECT_TRUE(notifier.send_txs(txs, context->get_id(), events_, cryptonote::relay_method::local)); + + io_service_.reset(); + ASSERT_LT(0u, io_service_.poll()); + notifier.run_fluff(); + io_service_.reset(); + ASSERT_LT(0u, io_service_.poll()); + + EXPECT_EQ(txs, events_.take_relayed(cryptonote::relay_method::local)); + + EXPECT_EQ(0u, context->process_send_queue()); + for (++context; context != contexts_.end(); ++context) + { + const bool is_incoming = ((context - contexts_.begin()) % 2 == 0); + EXPECT_EQ(is_incoming ? 0u : 1u, context->process_send_queue()); + } + + ASSERT_EQ(5u, receiver_.notified_size()); + for (unsigned count = 0; count < 5; ++count) + { + auto notification = receiver_.get_notification().second; + EXPECT_EQ(txs, notification.txs); + EXPECT_TRUE(notification._.empty()); + EXPECT_FALSE(notification.dandelionpp_fluff); + } + } +} + +TEST_F(levin_notify, private_block_without_padding) +{ + // private mode always uses fluff but marked as stem + cryptonote::levin::notify notifier = make_notifier(0, false, false); + + for (unsigned count = 0; count < 10; ++count) + add_connection(count % 2 == 0); + + { + const auto status = notifier.get_status(); + EXPECT_FALSE(status.has_noise); + EXPECT_FALSE(status.connections_filled); + } + notifier.new_out_connection(); + io_service_.poll(); + + std::vector txs(2); + txs[0].resize(100, 'e'); + txs[1].resize(200, 'f'); + + ASSERT_EQ(10u, contexts_.size()); + { + auto context = contexts_.begin(); + EXPECT_FALSE(notifier.send_txs(txs, context->get_id(), events_, cryptonote::relay_method::block)); + + io_service_.reset(); + ASSERT_EQ(0u, io_service_.poll()); + } +} + +TEST_F(levin_notify, private_none_without_padding) +{ + // private mode always uses fluff but marked as stem + cryptonote::levin::notify notifier = make_notifier(0, false, false); + + for (unsigned count = 0; count < 10; ++count) + add_connection(count % 2 == 0); + + { + const auto status = notifier.get_status(); + EXPECT_FALSE(status.has_noise); + EXPECT_FALSE(status.connections_filled); + } + notifier.new_out_connection(); + io_service_.poll(); + + std::vector txs(2); + txs[0].resize(100, 'e'); + txs[1].resize(200, 'f'); + + ASSERT_EQ(10u, contexts_.size()); + { + auto context = contexts_.begin(); + EXPECT_FALSE(notifier.send_txs(txs, context->get_id(), events_, cryptonote::relay_method::none)); + + io_service_.reset(); + ASSERT_EQ(0u, io_service_.poll()); + } +} + +TEST_F(levin_notify, private_fluff_with_padding) +{ + cryptonote::levin::notify notifier = make_notifier(0, false, true); + + for (unsigned count = 0; count < 10; ++count) + add_connection(count % 2 == 0); + + { + const auto status = notifier.get_status(); + EXPECT_FALSE(status.has_noise); + EXPECT_FALSE(status.connections_filled); + } + notifier.new_out_connection(); + io_service_.poll(); + + std::vector txs(2); + txs[0].resize(100, 'e'); + txs[1].resize(200, 'f'); + + ASSERT_EQ(10u, contexts_.size()); + { + auto context = contexts_.begin(); + EXPECT_TRUE(notifier.send_txs(txs, context->get_id(), events_, cryptonote::relay_method::fluff)); + + io_service_.reset(); + ASSERT_LT(0u, io_service_.poll()); + notifier.run_fluff(); + io_service_.reset(); + ASSERT_LT(0u, io_service_.poll()); + + EXPECT_EQ(txs, events_.take_relayed(cryptonote::relay_method::fluff)); + + EXPECT_EQ(0u, context->process_send_queue()); + for (++context; context != contexts_.end(); ++context) + { + const bool is_incoming = ((context - contexts_.begin()) % 2 == 0); + EXPECT_EQ(is_incoming ? 0u : 1u, context->process_send_queue()); + } + + ASSERT_EQ(5u, receiver_.notified_size()); + for (unsigned count = 0; count < 5; ++count) + { + auto notification = receiver_.get_notification().second; + EXPECT_EQ(txs, notification.txs); + EXPECT_FALSE(notification._.empty()); + EXPECT_FALSE(notification.dandelionpp_fluff); + } + } +} + +TEST_F(levin_notify, private_stem_with_padding) +{ + cryptonote::levin::notify notifier = make_notifier(0, false, true); + + for (unsigned count = 0; count < 10; ++count) + add_connection(count % 2 == 0); + + { + const auto status = notifier.get_status(); + EXPECT_FALSE(status.has_noise); + EXPECT_FALSE(status.connections_filled); + } + notifier.new_out_connection(); + io_service_.poll(); + + std::vector txs(2); + txs[0].resize(100, 'e'); + txs[1].resize(200, 'f'); + + ASSERT_EQ(10u, contexts_.size()); + { + auto context = contexts_.begin(); + EXPECT_TRUE(notifier.send_txs(txs, context->get_id(), events_, cryptonote::relay_method::stem)); + + io_service_.reset(); + ASSERT_LT(0u, io_service_.poll()); + notifier.run_fluff(); + io_service_.reset(); + ASSERT_LT(0u, io_service_.poll()); + + EXPECT_EQ(txs, events_.take_relayed(cryptonote::relay_method::fluff)); + + EXPECT_EQ(0u, context->process_send_queue()); + for (++context; context != contexts_.end(); ++context) + { + const bool is_incoming = ((context - contexts_.begin()) % 2 == 0); + EXPECT_EQ(is_incoming ? 0u : 1u, context->process_send_queue()); + } + + ASSERT_EQ(5u, receiver_.notified_size()); + for (unsigned count = 0; count < 5; ++count) + { + auto notification = receiver_.get_notification().second; + EXPECT_EQ(txs, notification.txs); + EXPECT_FALSE(notification._.empty()); + EXPECT_FALSE(notification.dandelionpp_fluff); + } + } +} + +TEST_F(levin_notify, private_local_with_padding) +{ + cryptonote::levin::notify notifier = make_notifier(0, false, true); + + for (unsigned count = 0; count < 10; ++count) + add_connection(count % 2 == 0); + + { + const auto status = notifier.get_status(); + EXPECT_FALSE(status.has_noise); + EXPECT_FALSE(status.connections_filled); + } + notifier.new_out_connection(); + io_service_.poll(); + + std::vector txs(2); + txs[0].resize(100, 'e'); + txs[1].resize(200, 'f'); + + ASSERT_EQ(10u, contexts_.size()); + { + auto context = contexts_.begin(); + EXPECT_TRUE(notifier.send_txs(txs, context->get_id(), events_, cryptonote::relay_method::local)); + + io_service_.reset(); + ASSERT_LT(0u, io_service_.poll()); + notifier.run_fluff(); + io_service_.reset(); + ASSERT_LT(0u, io_service_.poll()); + + EXPECT_EQ(txs, events_.take_relayed(cryptonote::relay_method::local)); + + EXPECT_EQ(0u, context->process_send_queue()); + for (++context; context != contexts_.end(); ++context) + { + const bool is_incoming = ((context - contexts_.begin()) % 2 == 0); + EXPECT_EQ(is_incoming ? 0u : 1u, context->process_send_queue()); + } + + ASSERT_EQ(5u, receiver_.notified_size()); + for (unsigned count = 0; count < 5; ++count) + { + auto notification = receiver_.get_notification().second; + EXPECT_EQ(txs, notification.txs); + EXPECT_FALSE(notification._.empty()); + EXPECT_FALSE(notification.dandelionpp_fluff); + } + } +} + +TEST_F(levin_notify, private_block_with_padding) +{ + cryptonote::levin::notify notifier = make_notifier(0, false, true); + + for (unsigned count = 0; count < 10; ++count) + add_connection(count % 2 == 0); + + { + const auto status = notifier.get_status(); + EXPECT_FALSE(status.has_noise); + EXPECT_FALSE(status.connections_filled); + } + notifier.new_out_connection(); + io_service_.poll(); + + std::vector txs(2); + txs[0].resize(100, 'e'); + txs[1].resize(200, 'f'); + + ASSERT_EQ(10u, contexts_.size()); + { + auto context = contexts_.begin(); + EXPECT_FALSE(notifier.send_txs(txs, context->get_id(), events_, cryptonote::relay_method::block)); + + io_service_.reset(); + ASSERT_EQ(0u, io_service_.poll()); + } +} + +TEST_F(levin_notify, private_none_with_padding) +{ + cryptonote::levin::notify notifier = make_notifier(0, false, true); + + for (unsigned count = 0; count < 10; ++count) + add_connection(count % 2 == 0); + + { + const auto status = notifier.get_status(); + EXPECT_FALSE(status.has_noise); + EXPECT_FALSE(status.connections_filled); + } + notifier.new_out_connection(); + io_service_.poll(); + + std::vector txs(2); + txs[0].resize(100, 'e'); + txs[1].resize(200, 'f'); + + ASSERT_EQ(10u, contexts_.size()); + { + auto context = contexts_.begin(); + EXPECT_FALSE(notifier.send_txs(txs, context->get_id(), events_, cryptonote::relay_method::none)); + + io_service_.reset(); + ASSERT_EQ(0u, io_service_.poll()); + } +} + +TEST_F(levin_notify, stem_mappings) +{ + static constexpr const unsigned test_connections_count = (CRYPTONOTE_DANDELIONPP_STEMS + 1) * 2; + + cryptonote::levin::notify notifier = make_notifier(0, true, false); + + for (unsigned count = 0; count < test_connections_count; ++count) + add_connection(count % 2 == 0); + + { + const auto status = notifier.get_status(); + EXPECT_FALSE(status.has_noise); + EXPECT_FALSE(status.connections_filled); + } + notifier.new_out_connection(); + io_service_.poll(); + + std::vector txs(2); + txs[0].resize(100, 'e'); + txs[1].resize(200, 'f'); + + ASSERT_EQ(test_connections_count, contexts_.size()); + for (;;) + { + auto context = contexts_.begin(); + EXPECT_TRUE(notifier.send_txs(txs, context->get_id(), events_, cryptonote::relay_method::stem)); + + io_service_.reset(); + ASSERT_LT(0u, io_service_.poll()); + if (events_.has_stem_txes()) + break; + + EXPECT_EQ(txs, events_.take_relayed(cryptonote::relay_method::fluff)); + notifier.run_fluff(); + io_service_.reset(); + ASSERT_LT(0u, io_service_.poll()); + + EXPECT_EQ(0u, context->process_send_queue()); + for (++context; context != contexts_.end(); ++context) + EXPECT_EQ(1u, context->process_send_queue()); + + ASSERT_EQ(test_connections_count - 1, receiver_.notified_size()); + for (unsigned count = 0; count < test_connections_count - 1; ++count) + { + auto notification = receiver_.get_notification().second; + EXPECT_EQ(txs, notification.txs); + EXPECT_TRUE(notification._.empty()); + EXPECT_TRUE(notification.dandelionpp_fluff); + } + + notifier.run_epoch(); + io_service_.reset(); + ASSERT_LT(0u, io_service_.poll()); + } + EXPECT_EQ(txs, events_.take_relayed(cryptonote::relay_method::stem)); + + std::set used; + std::map mappings; + { + std::size_t send_count = 0; + for (auto context = contexts_.begin(); context != contexts_.end(); ++context) + { + const std::size_t sent = context->process_send_queue(); + if (sent) + { + EXPECT_EQ(1u, (context - contexts_.begin()) % 2); + EXPECT_FALSE(context->is_incoming()); + used.insert(context->get_id()); + mappings[contexts_.front().get_id()] = context->get_id(); + } + send_count += sent; + } + + EXPECT_EQ(1u, send_count); + ASSERT_EQ(1u, receiver_.notified_size()); + for (unsigned count = 0; count < 1u; ++count) + { + auto notification = receiver_.get_notification().second; + EXPECT_EQ(txs, notification.txs); + EXPECT_TRUE(notification._.empty()); + EXPECT_FALSE(notification.dandelionpp_fluff); + } + } + + for (unsigned i = 0; i < contexts_.size() * 2; i += 2) + { + auto& incoming = contexts_[i % contexts_.size()]; + EXPECT_TRUE(notifier.send_txs(txs, incoming.get_id(), events_, cryptonote::relay_method::stem)); + + io_service_.reset(); + ASSERT_LT(0u, io_service_.poll()); + EXPECT_EQ(txs, events_.take_relayed(cryptonote::relay_method::stem)); + + std::size_t send_count = 0; + for (auto context = contexts_.begin(); context != contexts_.end(); ++context) + { + const std::size_t sent = context->process_send_queue(); + if (sent) + { + EXPECT_EQ(1u, (context - contexts_.begin()) % 2); + EXPECT_FALSE(context->is_incoming()); + used.insert(context->get_id()); + + auto inserted = mappings.emplace(incoming.get_id(), context->get_id()).first; + EXPECT_EQ(inserted->second, context->get_id()) << "incoming index " << i; + } + send_count += sent; + } + + EXPECT_EQ(1u, send_count); + ASSERT_EQ(1u, receiver_.notified_size()); + for (unsigned count = 0; count < 1u; ++count) + { + auto notification = receiver_.get_notification().second; + EXPECT_EQ(txs, notification.txs); + EXPECT_TRUE(notification._.empty()); + EXPECT_FALSE(notification.dandelionpp_fluff); + } + } + + EXPECT_EQ(CRYPTONOTE_DANDELIONPP_STEMS, used.size()); +} + +TEST_F(levin_notify, fluff_multiple) +{ + static constexpr const unsigned test_connections_count = (CRYPTONOTE_DANDELIONPP_STEMS + 1) * 2; + + cryptonote::levin::notify notifier = make_notifier(0, true, false); + + for (unsigned count = 0; count < test_connections_count; ++count) + add_connection(count % 2 == 0); + + { + const auto status = notifier.get_status(); + EXPECT_FALSE(status.has_noise); + EXPECT_FALSE(status.connections_filled); + } + notifier.new_out_connection(); + io_service_.poll(); + + std::vector txs(2); + txs[0].resize(100, 'e'); + txs[1].resize(200, 'f'); + + ASSERT_EQ(test_connections_count, contexts_.size()); + for (;;) + { + auto context = contexts_.begin(); + EXPECT_TRUE(notifier.send_txs(txs, context->get_id(), events_, cryptonote::relay_method::stem)); + + io_service_.reset(); + ASSERT_LT(0u, io_service_.poll()); + if (!events_.has_stem_txes()) + break; + + EXPECT_EQ(txs, events_.take_relayed(cryptonote::relay_method::stem)); + + std::size_t send_count = 0; + EXPECT_EQ(0u, context->process_send_queue()); + for (++context; context != contexts_.end(); ++context) + { + const std::size_t sent = context->process_send_queue(); + if (sent) + { + EXPECT_EQ(1u, (context - contexts_.begin()) % 2); + EXPECT_FALSE(context->is_incoming()); + } + send_count += sent; + } + + EXPECT_EQ(1u, send_count); + ASSERT_EQ(1u, receiver_.notified_size()); + for (unsigned count = 0; count < 1; ++count) + { + auto notification = receiver_.get_notification().second; + EXPECT_EQ(txs, notification.txs); + EXPECT_TRUE(notification._.empty()); + EXPECT_FALSE(notification.dandelionpp_fluff); + } + + notifier.run_epoch(); + io_service_.reset(); + ASSERT_LT(0u, io_service_.poll()); + } + EXPECT_EQ(txs, events_.take_relayed(cryptonote::relay_method::fluff)); + notifier.run_fluff(); + io_service_.reset(); + ASSERT_LT(0u, io_service_.poll()); + { + auto context = contexts_.begin(); + EXPECT_EQ(0u, context->process_send_queue()); + for (++context; context != contexts_.end(); ++context) + EXPECT_EQ(1u, context->process_send_queue()); + + ASSERT_EQ(contexts_.size() - 1, receiver_.notified_size()); + for (unsigned count = 0; count < contexts_.size() - 1; ++count) + { + auto notification = receiver_.get_notification().second; + EXPECT_EQ(txs, notification.txs); + EXPECT_TRUE(notification._.empty()); + EXPECT_TRUE(notification.dandelionpp_fluff); + } + } + + for (unsigned i = 0; i < contexts_.size() * 2; i += 2) + { + auto& incoming = contexts_[i % contexts_.size()]; + EXPECT_TRUE(notifier.send_txs(txs, incoming.get_id(), events_, cryptonote::relay_method::stem)); + + io_service_.reset(); + ASSERT_LT(0u, io_service_.poll()); + notifier.run_fluff(); + io_service_.reset(); + ASSERT_LT(0u, io_service_.poll()); + + EXPECT_EQ(txs, events_.take_relayed(cryptonote::relay_method::fluff)); + + for (auto& context : contexts_) + { + if (std::addressof(incoming) == std::addressof(context)) + EXPECT_EQ(0u, context.process_send_queue()); + else + EXPECT_EQ(1u, context.process_send_queue()); + } + + ASSERT_EQ(contexts_.size() - 1, receiver_.notified_size()); + for (unsigned count = 0; count < contexts_.size() - 1; ++count) + { + auto notification = receiver_.get_notification().second; + EXPECT_EQ(txs, notification.txs); + EXPECT_TRUE(notification._.empty()); + EXPECT_TRUE(notification.dandelionpp_fluff); + } + } +} + +TEST_F(levin_notify, noise) +{ + for (unsigned count = 0; count < 10; ++count) + add_connection(count % 2 == 0); + + std::vector txs(1); + txs[0].resize(1900, 'h'); + + const boost::uuids::uuid incoming_id = random_generator_(); + cryptonote::levin::notify notifier = make_notifier(2048, false, true); + + { + const auto status = notifier.get_status(); + EXPECT_TRUE(status.has_noise); + EXPECT_FALSE(status.connections_filled); + } + ASSERT_LT(0u, io_service_.poll()); + { + const auto status = notifier.get_status(); + EXPECT_TRUE(status.has_noise); + EXPECT_TRUE(status.connections_filled); + } + + notifier.run_stems(); + io_service_.reset(); + ASSERT_LT(0u, io_service_.poll()); + { + std::size_t sent = 0; + for (auto& context : contexts_) + sent += context.process_send_queue(); + + EXPECT_EQ(2u, sent); + EXPECT_EQ(0u, receiver_.notified_size()); + } + + EXPECT_TRUE(notifier.send_txs(txs, incoming_id, events_, cryptonote::relay_method::local)); + notifier.run_stems(); + io_service_.reset(); + ASSERT_LT(0u, io_service_.poll()); + + EXPECT_EQ(txs, events_.take_relayed(cryptonote::relay_method::local)); + { + std::size_t sent = 0; + for (auto& context : contexts_) + sent += context.process_send_queue(); + + ASSERT_EQ(2u, sent); + while (sent--) + { + auto notification = receiver_.get_notification().second; + EXPECT_EQ(txs, notification.txs); + EXPECT_TRUE(notification._.empty()); + EXPECT_FALSE(notification.dandelionpp_fluff); + } + } + + txs[0].resize(3000, 'r'); + EXPECT_TRUE(notifier.send_txs(txs, incoming_id, events_, cryptonote::relay_method::fluff)); + notifier.run_stems(); + io_service_.reset(); + ASSERT_LT(0u, io_service_.poll()); + + EXPECT_EQ(txs, events_.take_relayed(cryptonote::relay_method::fluff)); + { + std::size_t sent = 0; + for (auto& context : contexts_) + sent += context.process_send_queue(); + + EXPECT_EQ(2u, sent); + EXPECT_EQ(0u, receiver_.notified_size()); + } + + notifier.run_stems(); + io_service_.reset(); + ASSERT_LT(0u, io_service_.poll()); + { + std::size_t sent = 0; + for (auto& context : contexts_) + sent += context.process_send_queue(); + + ASSERT_EQ(2u, sent); + while (sent--) + { + auto notification = receiver_.get_notification().second; + EXPECT_EQ(txs, notification.txs); + EXPECT_TRUE(notification._.empty()); + EXPECT_FALSE(notification.dandelionpp_fluff); + } + } +} + +TEST_F(levin_notify, noise_stem) +{ + for (unsigned count = 0; count < 10; ++count) + add_connection(count % 2 == 0); + + std::vector txs(1); + txs[0].resize(1900, 'h'); + + const boost::uuids::uuid incoming_id = random_generator_(); + cryptonote::levin::notify notifier = make_notifier(2048, false, true); + + { + const auto status = notifier.get_status(); + EXPECT_TRUE(status.has_noise); + EXPECT_FALSE(status.connections_filled); + } + ASSERT_LT(0u, io_service_.poll()); + { + const auto status = notifier.get_status(); + EXPECT_TRUE(status.has_noise); + EXPECT_TRUE(status.connections_filled); + } + + notifier.run_stems(); + io_service_.reset(); + ASSERT_LT(0u, io_service_.poll()); + { + std::size_t sent = 0; + for (auto& context : contexts_) + sent += context.process_send_queue(); + + EXPECT_EQ(2u, sent); + EXPECT_EQ(0u, receiver_.notified_size()); + } + + EXPECT_TRUE(notifier.send_txs(txs, incoming_id, events_, cryptonote::relay_method::stem)); + notifier.run_stems(); + io_service_.reset(); ASSERT_LT(0u, io_service_.poll()); + + // downgraded to local when being notified + EXPECT_EQ(txs, events_.take_relayed(cryptonote::relay_method::local)); { std::size_t sent = 0; for (auto& context : contexts_) @@ -726,6 +1772,7 @@ TEST_F(levin_notify, noise) auto notification = receiver_.get_notification().second; EXPECT_EQ(txs, notification.txs); EXPECT_TRUE(notification._.empty()); + EXPECT_FALSE(notification.dandelionpp_fluff); } } }