diff --git a/src/blockchain_db/blockchain_db.cpp b/src/blockchain_db/blockchain_db.cpp index 01faf43c4..336577a07 100644 --- a/src/blockchain_db/blockchain_db.cpp +++ b/src/blockchain_db/blockchain_db.cpp @@ -63,6 +63,7 @@ bool matches_category(relay_method method, relay_category category) noexcept { default: case relay_method::local: + case relay_method::forward: case relay_method::stem: return false; case relay_method::block: @@ -79,6 +80,7 @@ void txpool_tx_meta_t::set_relay_method(relay_method method) noexcept kept_by_block = 0; do_not_relay = 0; is_local = 0; + is_forwarding = 0; dandelionpp_stem = 0; switch (method) @@ -89,8 +91,8 @@ void txpool_tx_meta_t::set_relay_method(relay_method method) noexcept case relay_method::local: is_local = 1; break; - default: - case relay_method::fluff: + case relay_method::forward: + is_forwarding = 1; break; case relay_method::stem: dandelionpp_stem = 1; @@ -98,26 +100,45 @@ void txpool_tx_meta_t::set_relay_method(relay_method method) noexcept case relay_method::block: kept_by_block = 1; break; + default: + case relay_method::fluff: + break; } } relay_method txpool_tx_meta_t::get_relay_method() const noexcept { - if (kept_by_block) - return relay_method::block; - if (do_not_relay) - return relay_method::none; - if (is_local) - return relay_method::local; - if (dandelionpp_stem) - return relay_method::stem; + const uint8_t state = + uint8_t(kept_by_block) + + (uint8_t(do_not_relay) << 1) + + (uint8_t(is_local) << 2) + + (uint8_t(is_forwarding) << 3) + + (uint8_t(dandelionpp_stem) << 4); + + switch (state) + { + default: // error case + case 0: + break; + case 1: + return relay_method::block; + case 2: + return relay_method::none; + case 4: + return relay_method::local; + case 8: + return relay_method::forward; + case 16: + 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::local < relay_method::forward, "bad relay_method value"); + static_assert(relay_method::forward < 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"); diff --git a/src/blockchain_db/blockchain_db.h b/src/blockchain_db/blockchain_db.h index 3e2387da4..ac2c55db1 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; //!< If Dandelion++ stem, randomized embargo timestamp. Otherwise, last relayed timestmap. + uint64_t last_relayed_time; //!< If received over i2p/tor, randomized forward time. If Dandelion++stem, randomized embargo time. Otherwise, last relayed timestamp // 112 bytes uint8_t kept_by_block; uint8_t relayed; @@ -169,7 +169,8 @@ struct txpool_tx_meta_t uint8_t pruned: 1; uint8_t is_local: 1; uint8_t dandelionpp_stem : 1; - uint8_t bf_padding: 4; + uint8_t is_forwarding: 1; + uint8_t bf_padding: 3; uint8_t padding[76]; // till 192 bytes diff --git a/src/cryptonote_config.h b/src/cryptonote_config.h index 81dc15dee..401cca1ca 100644 --- a/src/cryptonote_config.h +++ b/src/cryptonote_config.h @@ -117,6 +117,11 @@ #define CRYPTONOTE_NOISE_BYTES 3*1024 // 3 KiB #define CRYPTONOTE_NOISE_CHANNELS 2 // Max outgoing connections per zone used for noise/covert sending +// Both below are in seconds. The idea is to delay forwarding from i2p/tor +// to ipv4/6, such that 2+ incoming connections _could_ have sent the tx +#define CRYPTONOTE_FORWARD_DELAY_BASE (CRYPTONOTE_NOISE_MIN_DELAY + CRYPTONOTE_NOISE_DELAY_RANGE) +#define CRYPTONOTE_FORWARD_DELAY_AVERAGE (CRYPTONOTE_FORWARD_DELAY_BASE + (CRYPTONOTE_FORWARD_DELAY_BASE / 2)) + #define CRYPTONOTE_MAX_FRAGMENTS 20 // ~20 * NOISE_BYTES max payload size for covert/noise send #define COMMAND_RPC_GET_BLOCKS_FAST_MAX_COUNT 1000 diff --git a/src/cryptonote_core/cryptonote_core.cpp b/src/cryptonote_core/cryptonote_core.cpp index 3ff3c77e2..521734bd2 100644 --- a/src/cryptonote_core/cryptonote_core.cpp +++ b/src/cryptonote_core/cryptonote_core.cpp @@ -1272,6 +1272,7 @@ namespace cryptonote { NOTIFY_NEW_TRANSACTIONS::request public_req{}; NOTIFY_NEW_TRANSACTIONS::request private_req{}; + NOTIFY_NEW_TRANSACTIONS::request stem_req{}; for (auto& tx : txs) { switch (std::get<2>(tx)) @@ -1282,6 +1283,9 @@ namespace cryptonote case relay_method::local: private_req.txs.push_back(std::move(std::get<1>(tx))); break; + case relay_method::forward: + stem_req.txs.push_back(std::move(std::get<1>(tx))); + break; case relay_method::block: case relay_method::fluff: case relay_method::stem: @@ -1299,6 +1303,8 @@ namespace cryptonote 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, relay_method::local); + if (!stem_req.txs.empty()) + get_protocol()->relay_transactions(stem_req, source, epee::net_utils::zone::public_, relay_method::stem); } return true; } diff --git a/src/cryptonote_core/tx_pool.cpp b/src/cryptonote_core/tx_pool.cpp index a7b2e4422..542dbeb03 100644 --- a/src/cryptonote_core/tx_pool.cpp +++ b/src/cryptonote_core/tx_pool.cpp @@ -91,6 +91,8 @@ namespace cryptonote time_t const MAX_RELAY_TIME = (60 * 60 * 4); // at most that many seconds between resends float const ACCEPT_THRESHOLD = 1.0f; + constexpr const std::chrono::seconds forward_delay_average{CRYPTONOTE_FORWARD_DELAY_AVERAGE}; + // a kind of increasing backoff within min/max bounds uint64_t get_relay_delay(time_t now, time_t received) { @@ -309,8 +311,14 @@ namespace cryptonote if (meta.upgrade_relay_method(tx_relay) || !existing_tx) // synchronize with embargo timer or stem/fluff out-of-order messages { + using clock = std::chrono::system_clock; + auto last_relayed_time = std::numeric_limits::max(); + if (tx_relay == relay_method::forward) + last_relayed_time = clock::to_time_t(clock::now() + crypto::random_poisson_seconds{forward_delay_average}()); + // else the `set_relayed` function will adjust the time accordingly later + //update transactions container - meta.last_relayed_time = std::numeric_limits::max(); + meta.last_relayed_time = last_relayed_time; meta.receive_time = receive_time; meta.weight = tx_weight; meta.fee = fee; @@ -341,7 +349,7 @@ namespace cryptonote tvc.m_added_to_pool = true; static_assert(unsigned(relay_method::none) == 0, "expected relay_method::none value to be zero"); - if(meta.fee > 0) + if(meta.fee > 0 && tx_relay != relay_method::forward) tvc.m_relay = tx_relay; } @@ -722,28 +730,46 @@ namespace cryptonote //TODO: investigate whether boolean return is appropriate bool tx_memory_pool::get_relayable_transactions(std::vector> &txs) const { + std::vector> change_timestamps; + const uint64_t now = time(NULL); + CRITICAL_REGION_LOCAL(m_transactions_lock); CRITICAL_REGION_LOCAL1(m_blockchain); - const uint64_t now = time(NULL); + LockedTXN lock(m_blockchain.get_db()); 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 *){ + m_blockchain.for_all_txpool_txes([this, now, &txs, &change_timestamps](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) { - 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; + const relay_method tx_relay = meta.get_relay_method(); + switch (tx_relay) + { + case relay_method::stem: + case relay_method::forward: + if (meta.last_relayed_time > now) + return true; // continue to next tx + change_timestamps.emplace_back(txid, meta); + break; + default: + case relay_method::none: + return true; + case relay_method::local: + case relay_method::fluff: + case relay_method::block: + if (now - meta.last_relayed_time <= get_relay_delay(now, meta.receive_time)) + return true; // continue to next tx + break; + } // 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 - uint64_t max_age = meta.kept_by_block ? CRYPTONOTE_MEMPOOL_TX_FROM_ALT_BLOCK_LIVETIME : CRYPTONOTE_MEMPOOL_TX_LIVETIME; + uint64_t max_age = (tx_relay == relay_method::block) ? CRYPTONOTE_MEMPOOL_TX_FROM_ALT_BLOCK_LIVETIME : CRYPTONOTE_MEMPOOL_TX_LIVETIME; if (now - meta.receive_time <= max_age / 2) { try { - txs.emplace_back(txid, m_blockchain.get_txpool_tx_blob(txid, relay_category::all), meta.get_relay_method()); + txs.emplace_back(txid, m_blockchain.get_txpool_tx_blob(txid, relay_category::all), tx_relay); } catch (const std::exception &e) { @@ -754,6 +780,18 @@ namespace cryptonote } return true; }, false, relay_category::relayable); + + for (auto& elem : change_timestamps) + { + /* These transactions are still in forward or stem state, so the field + represents the next time a relay should be attempted. Will be + overwritten when the state is upgraded to stem, fluff or block. This + function is only called every ~2 minutes, so this resetting should be + unnecessary, but is primarily a precaution against potential changes + to the callback routines. */ + elem.second.last_relayed_time = now + get_relay_delay(now, elem.second.receive_time); + m_blockchain.update_txpool_tx(elem.first, elem.second); + } return true; } //--------------------------------------------------------------------------------- diff --git a/src/cryptonote_protocol/cryptonote_protocol_handler.inl b/src/cryptonote_protocol/cryptonote_protocol_handler.inl index f8e032fde..ef3ea837d 100644 --- a/src/cryptonote_protocol/cryptonote_protocol_handler.inl +++ b/src/cryptonote_protocol/cryptonote_protocol_handler.inl @@ -927,7 +927,19 @@ namespace cryptonote return 1; } - relay_method tx_relay; + /* If the txes were received over i2p/tor, the default is to "forward" + with a randomized delay to further enhance the "white noise" behavior, + potentially making it harder for ISP-level spies to determine which + inbound link sent the tx. If the sender disabled "white noise" over + i2p/tor, then the sender is "fluffing" (to only outbound) i2p/tor + connections with the `dandelionpp_fluff` flag set. The receiver (hidden + service) will immediately fluff in that scenario (i.e. this assumes that a + sybil spy will be unable to link an IP to an i2p/tor connection). */ + + const epee::net_utils::zone zone = context.m_remote_address.get_zone(); + relay_method tx_relay = zone == epee::net_utils::zone::public_ ? + relay_method::stem : relay_method::forward; + std::vector stem_txs{}; std::vector fluff_txs{}; if (arg.dandelionpp_fluff) @@ -936,10 +948,7 @@ namespace cryptonote fluff_txs.reserve(arg.txs.size()); } else - { - tx_relay = relay_method::stem; stem_txs.reserve(arg.txs.size()); - } for (auto& tx : arg.txs) { @@ -962,6 +971,7 @@ namespace cryptonote fluff_txs.push_back(std::move(tx)); break; default: + case relay_method::forward: // not supposed to happen here case relay_method::none: break; } diff --git a/src/cryptonote_protocol/enums.h b/src/cryptonote_protocol/enums.h index a3a52b83f..36e8ca04c 100644 --- a/src/cryptonote_protocol/enums.h +++ b/src/cryptonote_protocol/enums.h @@ -37,6 +37,7 @@ namespace cryptonote { none = 0, //!< Received via RPC with `do_not_relay` set local, //!< Received via RPC; trying to send over i2p/tor, etc. + forward, //!< Received over i2p/tor; timer delayed before ipv4/6 public broadcast 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 127801092..0ff194986 100644 --- a/src/cryptonote_protocol/levin_notify.cpp +++ b/src/cryptonote_protocol/levin_notify.cpp @@ -357,11 +357,15 @@ namespace levin return true; }); - // Always send txs in stem mode over i2p/tor, see comments in `send_txs` below. + /* Always send with `fluff` flag, even over i2p/tor. The hidden service + will disable the forwarding delay and immediately fluff. The i2p/tor + network is therefore replacing the sybil protection of Dandelion++. + Dandelion++ stem phase over i2p/tor is also worth investigating + (with/without "noise"?). */ 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, zone_->is_public); + make_payload_send_txs(*zone_->p2p, std::move(connection.first), connection.second, zone_->pad_txs, true); } if (next_flush != std::chrono::steady_clock::time_point::max()) @@ -811,12 +815,11 @@ namespace levin 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::forward: case relay_method::local: if (zone_->is_public) { - // this will change a local tx to stem or fluff ... + // this will change a local/forward tx to stem or fluff ... zone_->strand.dispatch( dandelionpp_notify{zone_, std::addressof(core), std::move(txs), source} ); @@ -824,6 +827,11 @@ namespace levin } /* fallthrough */ case relay_method::fluff: + /* If sending stem/forward/local txes over non public networks, + continue to claim that relay mode even though it used the "fluff" + routine. A "fluff" over i2p/tor is not the same as a "fluff" over + ipv4/6. Marking it as "fluff" here will make the tx immediately + visible externally from this node, which is not desired. */ core.on_transactions_relayed(epee::to_span(txs), tx_relay); zone_->strand.dispatch(fluff_notify{zone_, std::move(txs), source}); break; diff --git a/tests/unit_tests/levin.cpp b/tests/unit_tests/levin.cpp index 62b101a42..1f6971ce8 100644 --- a/tests/unit_tests/levin.cpp +++ b/tests/unit_tests/levin.cpp @@ -676,6 +676,76 @@ TEST_F(levin_notify, local_without_padding) } } +TEST_F(levin_notify, forward_without_padding) +{ + 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, '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(), events_, cryptonote::relay_method::forward)); + + 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); + 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; + 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, block_without_padding) { cryptonote::levin::notify notifier = make_notifier(0, true, false); @@ -914,6 +984,73 @@ TEST_F(levin_notify, local_with_padding) } } +TEST_F(levin_notify, forward_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()); + 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(), events_, cryptonote::relay_method::forward)); + + 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(); + } +} + TEST_F(levin_notify, block_with_padding) { cryptonote::levin::notify notifier = make_notifier(0, true, true); @@ -1017,7 +1154,7 @@ TEST_F(levin_notify, private_fluff_without_padding) auto notification = receiver_.get_notification().second; EXPECT_EQ(txs, notification.txs); EXPECT_TRUE(notification._.empty()); - EXPECT_FALSE(notification.dandelionpp_fluff); + EXPECT_TRUE(notification.dandelionpp_fluff); } } } @@ -1053,7 +1190,7 @@ TEST_F(levin_notify, private_stem_without_padding) io_service_.reset(); ASSERT_LT(0u, io_service_.poll()); - EXPECT_EQ(txs, events_.take_relayed(cryptonote::relay_method::fluff)); + EXPECT_EQ(txs, events_.take_relayed(cryptonote::relay_method::stem)); EXPECT_EQ(0u, context->process_send_queue()); for (++context; context != contexts_.end(); ++context) @@ -1068,7 +1205,7 @@ TEST_F(levin_notify, private_stem_without_padding) auto notification = receiver_.get_notification().second; EXPECT_EQ(txs, notification.txs); EXPECT_TRUE(notification._.empty()); - EXPECT_FALSE(notification.dandelionpp_fluff); + EXPECT_TRUE(notification.dandelionpp_fluff); } } } @@ -1119,7 +1256,58 @@ TEST_F(levin_notify, private_local_without_padding) auto notification = receiver_.get_notification().second; EXPECT_EQ(txs, notification.txs); EXPECT_TRUE(notification._.empty()); - EXPECT_FALSE(notification.dandelionpp_fluff); + EXPECT_TRUE(notification.dandelionpp_fluff); + } + } +} + +TEST_F(levin_notify, private_forward_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::forward)); + + 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::forward)); + + 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_TRUE(notification.dandelionpp_fluff); } } } @@ -1229,7 +1417,7 @@ TEST_F(levin_notify, private_fluff_with_padding) auto notification = receiver_.get_notification().second; EXPECT_EQ(txs, notification.txs); EXPECT_FALSE(notification._.empty()); - EXPECT_FALSE(notification.dandelionpp_fluff); + EXPECT_TRUE(notification.dandelionpp_fluff); } } } @@ -1264,7 +1452,7 @@ TEST_F(levin_notify, private_stem_with_padding) io_service_.reset(); ASSERT_LT(0u, io_service_.poll()); - EXPECT_EQ(txs, events_.take_relayed(cryptonote::relay_method::fluff)); + EXPECT_EQ(txs, events_.take_relayed(cryptonote::relay_method::stem)); EXPECT_EQ(0u, context->process_send_queue()); for (++context; context != contexts_.end(); ++context) @@ -1279,7 +1467,7 @@ TEST_F(levin_notify, private_stem_with_padding) auto notification = receiver_.get_notification().second; EXPECT_EQ(txs, notification.txs); EXPECT_FALSE(notification._.empty()); - EXPECT_FALSE(notification.dandelionpp_fluff); + EXPECT_TRUE(notification.dandelionpp_fluff); } } } @@ -1329,7 +1517,57 @@ TEST_F(levin_notify, private_local_with_padding) auto notification = receiver_.get_notification().second; EXPECT_EQ(txs, notification.txs); EXPECT_FALSE(notification._.empty()); - EXPECT_FALSE(notification.dandelionpp_fluff); + EXPECT_TRUE(notification.dandelionpp_fluff); + } + } +} + +TEST_F(levin_notify, private_forward_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::forward)); + + 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::forward)); + + 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_TRUE(notification.dandelionpp_fluff); } } }