|
|
|
@ -30,6 +30,7 @@
|
|
|
|
|
|
|
|
|
|
#include <boost/asio/steady_timer.hpp>
|
|
|
|
|
#include <boost/system/system_error.hpp>
|
|
|
|
|
#include <boost/uuid/uuid_io.hpp>
|
|
|
|
|
#include <chrono>
|
|
|
|
|
#include <deque>
|
|
|
|
|
#include <stdexcept>
|
|
|
|
@ -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<fluff_stepsize::rep> 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<blobdata>&& txs, const bool pad)
|
|
|
|
|
std::string make_tx_payload(std::vector<blobdata>&& 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<blobdata>&& txs, const boost::uuids::uuid& destination, const bool pad)
|
|
|
|
|
bool make_payload_send_txs(connections& p2p, std::vector<blobdata>&& 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<std::size_t> 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<detail::zone> zone, epee::span<const blobdata> 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<detail::zone> zone, std::vector<boost::uuids::uuid> 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<detail::zone> zone_;
|
|
|
|
|
i_core_events* core_;
|
|
|
|
|
std::vector<blobdata> 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<blobdata>{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<detail::zone> zone_;
|
|
|
|
|
net::dandelionpp::connection_map map_; // Requires manual copy constructor
|
|
|
|
|
bool fluffing_;
|
|
|
|
|
|
|
|
|
|
public:
|
|
|
|
|
explicit change_channels(std::shared_ptr<detail::zone> zone, net::dandelionpp::connection_map map)
|
|
|
|
|
: zone_(std::move(zone)), map_(std::move(map))
|
|
|
|
|
explicit change_channels(std::shared_ptr<detail::zone> 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<blobdata> txs, const boost::uuids::uuid& source)
|
|
|
|
|
bool notify::send_txs(std::vector<blobdata> 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<std::uint8_t>(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
|
|
|
|
|