forked from wownero/wownero
Merge pull request #5793
release-v0.7.1.0bdfc63a
Add ref-counted buffer byte_slice. Currently used for sending TCP data. (vtnerd)3b24b1d
Added support for 'noise' over I1P/Tor to mask Tx transmission. (vtnerd)
commit
98af2e954b
@ -0,0 +1,165 @@
|
|||||||
|
# Levin Protocol
|
||||||
|
This is a document explaining the current design of the levin protocol, as
|
||||||
|
used by Monero. The protocol is largely inherited from cryptonote, but has
|
||||||
|
undergone some changes.
|
||||||
|
|
||||||
|
This document also may differ from the `struct bucket_head2` in Monero's
|
||||||
|
code slightly - the spec here is slightly more strict to allow for
|
||||||
|
extensibility.
|
||||||
|
|
||||||
|
One of the goals of this document is to clearly indicate what is being sent
|
||||||
|
"on the wire" to identify metadata that could de-anonymize users over I2P/Tor.
|
||||||
|
These issues will be addressed as they are found. See `ANONMITY_NETWORKS.md` in
|
||||||
|
the top-level folder for any outstanding issues.
|
||||||
|
|
||||||
|
> This document does not currently list all data being sent by the monero
|
||||||
|
> protocol, that portion is a work-in-progress. Please take the time to do it
|
||||||
|
> if interested in learning about Monero p2p traffic!
|
||||||
|
|
||||||
|
|
||||||
|
## Header
|
||||||
|
This header is sent for every Monero p2p message.
|
||||||
|
|
||||||
|
```
|
||||||
|
0 1 2 3
|
||||||
|
0 1 2 3 4 5 6 7 0 1 2 3 4 5 6 7 0 1 2 3 4 5 6 7 0 1 2 3 4 5 6 7
|
||||||
|
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|
||||||
|
| 0x01 | 0x21 | 0x01 | 0x01 |
|
||||||
|
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|
||||||
|
| 0x01 | 0x01 | 0x01 | 0x01 |
|
||||||
|
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|
||||||
|
| Length |
|
||||||
|
| |
|
||||||
|
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|
||||||
|
| E. Response | Command
|
||||||
|
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|
||||||
|
| Return Code
|
||||||
|
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|
||||||
|
|Q|S|B|E| Reserved
|
||||||
|
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|
||||||
|
| 0x01 | 0x00 | 0x00 |
|
||||||
|
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|
||||||
|
| 0x00 |
|
||||||
|
+-+-+-+-+-+-+-+-+
|
||||||
|
```
|
||||||
|
|
||||||
|
### Signature
|
||||||
|
The first 8 bytes are the "signature" which helps identify the protocol (in
|
||||||
|
case someone connected to the wrong port, etc). The comments indicate that byte
|
||||||
|
sequence is from "benders nightmare".
|
||||||
|
|
||||||
|
This also can be used by deep packet inspection (DPI) engines to identify
|
||||||
|
Monero when the link is not encrypted. SSL has been proposed as a means to
|
||||||
|
mitigate this issue, but BIP-151 or the Noise protocol should also be considered.
|
||||||
|
|
||||||
|
### Length
|
||||||
|
The length is an unsigned 64-bit little endian integer. The length does _not_
|
||||||
|
include the header.
|
||||||
|
|
||||||
|
The implementation currently rejects received messages that exceed 100 MB
|
||||||
|
(base 10) by default.
|
||||||
|
|
||||||
|
### Expect Response
|
||||||
|
A zero-byte if no response is expected from the peer, and a non-zero byte if a
|
||||||
|
response is expected from the peer. Peers must respond to requests with this
|
||||||
|
flag in the same order that they were received, however, other messages can be
|
||||||
|
sent between responses.
|
||||||
|
|
||||||
|
There are some commands in the
|
||||||
|
[cryptonote protocol](#cryptonote-protocol-commands) where a response is
|
||||||
|
expected from the peer, but this flag is not set. Those responses are returned
|
||||||
|
as notify messages and can be sent in any order by the peer.
|
||||||
|
|
||||||
|
### Command
|
||||||
|
An unsigned 32-bit little endian integer representing the Monero specific
|
||||||
|
command being invoked.
|
||||||
|
|
||||||
|
### Return Code
|
||||||
|
A signed 32-bit little integer integer representing the response from the peer
|
||||||
|
from the last command that was invoked. This is `0` for request messages.
|
||||||
|
|
||||||
|
### Flags
|
||||||
|
* `Q` - Bit is set if the message is a request.
|
||||||
|
* `S` - Bit is set if the message is a response.
|
||||||
|
* `B` - Bit is set if this is a the beginning of a [fragmented message](#fragmented-messages).
|
||||||
|
* `E` - Bit is set if this is the end of a [fragmented message](#fragmented-messages).
|
||||||
|
|
||||||
|
### Version
|
||||||
|
A fixed value of `1` as an unsigned 32-bit little endian integer.
|
||||||
|
|
||||||
|
|
||||||
|
## Message Flow
|
||||||
|
The protocol can be subdivided into: (1) notifications, (2) requests,
|
||||||
|
(3) responses, (4) fragmented messages, and (5) dummy messages. Response
|
||||||
|
messages must be sent in the same order that a peer issued a request message.
|
||||||
|
A peer does not have to send a response immediately following a request - any
|
||||||
|
other message type can be sent instead.
|
||||||
|
|
||||||
|
### Notifications
|
||||||
|
Notifications are one-way messages that can be sent at any time without
|
||||||
|
an expectation of a response from the peer. The `Q` bit must be set, the `S`,
|
||||||
|
`B` and `E` bits must be unset, and the `Expect Response` field must be zeroed.
|
||||||
|
|
||||||
|
Some notifications must be in response to other notifications. This is not
|
||||||
|
part of the levin messaging layer, and is described in the
|
||||||
|
[commands](#commands) section.
|
||||||
|
|
||||||
|
### Requests
|
||||||
|
Requests are the basis of the admin protocol for Monero. The `Q` bit must be
|
||||||
|
set, the `S`, `B` and `E` bits must be unset, and the `Expect Response` field
|
||||||
|
must be non-zero. The peer is expected to send a response message with the same
|
||||||
|
`command` number.
|
||||||
|
|
||||||
|
### Responses
|
||||||
|
Response message can only be sent after a peer first issues a request message.
|
||||||
|
Responses must have the `S` bit set, the `Q`, `B` and `E` bits unset, and have
|
||||||
|
a zeroed `Expect Response` field. The `Command` field must be the same value
|
||||||
|
that was sent in the request message. The `Return Code` is specific to the
|
||||||
|
`Command` being issued (see [commands])(#commands)).
|
||||||
|
|
||||||
|
### Fragmented
|
||||||
|
Fragmented messages were introduced for the "white noise" feature for i2p/tor.
|
||||||
|
A transaction can be sent in fragments to conceal when "real" data is being
|
||||||
|
sent instead of dummy messages. Only one fragmented message can be sent at a
|
||||||
|
time, and bits `B` and `E` are never set at the same time
|
||||||
|
(see [dummy messages](#dummy)). The re-constructed message must contain a
|
||||||
|
levin header for a different (non-fragment) message type.
|
||||||
|
|
||||||
|
The `Q` and `S` bits are never set and the `Expect Response` field must always
|
||||||
|
be zero. The first fragment has the `B` bit set, neither `B` nor `E` is set for
|
||||||
|
"middle" fragments, and `E` is set for the last fragment.
|
||||||
|
|
||||||
|
### Dummy
|
||||||
|
Dummy messages have the `B` and `E` bits set, the `Q` and `S` bits unset, and
|
||||||
|
the `Expect Reponse` field zeroed. When a message of this type is received, the
|
||||||
|
contents can be safely ignored.
|
||||||
|
|
||||||
|
|
||||||
|
## Commands
|
||||||
|
### P2P (Admin) Commands
|
||||||
|
|
||||||
|
#### (`1001` Request) Handshake
|
||||||
|
#### (`1001` Response) Handshake
|
||||||
|
#### (`1002` Request) Timed Sync
|
||||||
|
#### (`1002` Response) Timed Sync
|
||||||
|
#### (`1003` Request) Ping
|
||||||
|
#### (`1003` Response) Ping
|
||||||
|
#### (`1004` Request) Stat Info
|
||||||
|
#### (`1004` Response) Stat Info
|
||||||
|
#### (`1005` Request) Network State
|
||||||
|
#### (`1005` Response) Network State
|
||||||
|
#### (`1006` Request) Peer ID
|
||||||
|
#### (`1006` Reponse) Peer ID
|
||||||
|
#### (`1007` Request) Support Flags
|
||||||
|
#### (`1007` Response) Support Flags
|
||||||
|
|
||||||
|
### Cryptonote Protocol Commands
|
||||||
|
|
||||||
|
#### (`2001` Notification) New Block
|
||||||
|
#### (`2002` Notification) New Transactions
|
||||||
|
#### (`2003` Notification) Request Get Objects
|
||||||
|
#### (`2004` Notification) Response Get Objects
|
||||||
|
#### (`2006` Notification) Request Chain
|
||||||
|
#### (`2007` Notification) Response Chain Entry
|
||||||
|
#### (`2008` Notification) New Fluffy Block
|
||||||
|
#### (`2009` Notification) Request Fluffy Missing TX
|
@ -0,0 +1,145 @@
|
|||||||
|
// Copyright (c) 2019, 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 <cstddef>
|
||||||
|
#include <cstdint>
|
||||||
|
#include <memory>
|
||||||
|
#include <string>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
#include "span.h"
|
||||||
|
|
||||||
|
namespace epee
|
||||||
|
{
|
||||||
|
struct byte_slice_data;
|
||||||
|
|
||||||
|
struct release_byte_slice
|
||||||
|
{
|
||||||
|
void operator()(byte_slice_data*) const noexcept;
|
||||||
|
};
|
||||||
|
|
||||||
|
/*! Inspired by slices in golang. Storage is thread-safe reference counted,
|
||||||
|
allowing for cheap copies or range selection on the bytes. The bytes
|
||||||
|
owned by this class are always immutable.
|
||||||
|
|
||||||
|
The functions `operator=`, `take_slice` and `remove_prefix` may alter the
|
||||||
|
reference count for the backing store, which will invalidate pointers
|
||||||
|
previously returned if the reference count is zero. Be careful about
|
||||||
|
"caching" pointers in these circumstances. */
|
||||||
|
class byte_slice
|
||||||
|
{
|
||||||
|
/* A custom reference count is used instead of shared_ptr because it allows
|
||||||
|
for an allocation optimization for the span constructor. This also
|
||||||
|
reduces the size of this class by one pointer. */
|
||||||
|
std::unique_ptr<byte_slice_data, release_byte_slice> storage_;
|
||||||
|
span<const std::uint8_t> portion_; // within storage_
|
||||||
|
|
||||||
|
//! Internal use only; use to increase `storage` reference count.
|
||||||
|
byte_slice(byte_slice_data* storage, span<const std::uint8_t> portion) noexcept;
|
||||||
|
|
||||||
|
struct adapt_buffer{};
|
||||||
|
|
||||||
|
template<typename T>
|
||||||
|
explicit byte_slice(const adapt_buffer, T&& buffer);
|
||||||
|
|
||||||
|
public:
|
||||||
|
using value_type = std::uint8_t;
|
||||||
|
using size_type = std::size_t;
|
||||||
|
using difference_type = std::ptrdiff_t;
|
||||||
|
using pointer = const std::uint8_t*;
|
||||||
|
using const_pointer = const std::uint8_t*;
|
||||||
|
using reference = std::uint8_t;
|
||||||
|
using const_reference = std::uint8_t;
|
||||||
|
using iterator = pointer;
|
||||||
|
using const_iterator = const_pointer;
|
||||||
|
|
||||||
|
//! Construct empty slice.
|
||||||
|
byte_slice() noexcept
|
||||||
|
: storage_(nullptr), portion_()
|
||||||
|
{}
|
||||||
|
|
||||||
|
//! Construct empty slice
|
||||||
|
byte_slice(std::nullptr_t) noexcept
|
||||||
|
: byte_slice()
|
||||||
|
{}
|
||||||
|
|
||||||
|
//! Scatter-gather (copy) multiple `sources` into a single allocated slice.
|
||||||
|
explicit byte_slice(std::initializer_list<span<const std::uint8_t>> sources);
|
||||||
|
|
||||||
|
//! Convert `buffer` into a slice using one allocation for shared count.
|
||||||
|
explicit byte_slice(std::vector<std::uint8_t>&& buffer);
|
||||||
|
|
||||||
|
//! Convert `buffer` into a slice using one allocation for shared count.
|
||||||
|
explicit byte_slice(std::string&& buffer);
|
||||||
|
|
||||||
|
byte_slice(byte_slice&& source) noexcept;
|
||||||
|
~byte_slice() noexcept = default;
|
||||||
|
|
||||||
|
//! \note May invalidate previously retrieved pointers.
|
||||||
|
byte_slice& operator=(byte_slice&&) noexcept;
|
||||||
|
|
||||||
|
//! \return A shallow (cheap) copy of the data from `this` slice.
|
||||||
|
byte_slice clone() const noexcept { return {storage_.get(), portion_}; }
|
||||||
|
|
||||||
|
iterator begin() const noexcept { return portion_.begin(); }
|
||||||
|
const_iterator cbegin() const noexcept { return portion_.begin(); }
|
||||||
|
|
||||||
|
iterator end() const noexcept { return portion_.end(); }
|
||||||
|
const_iterator cend() const noexcept { return portion_.end(); }
|
||||||
|
|
||||||
|
bool empty() const noexcept { return storage_ == nullptr; }
|
||||||
|
const std::uint8_t* data() const noexcept { return portion_.data(); }
|
||||||
|
std::size_t size() const noexcept { return portion_.size(); }
|
||||||
|
|
||||||
|
/*! Drop bytes from the beginning of `this` slice.
|
||||||
|
|
||||||
|
\note May invalidate previously retrieved pointers.
|
||||||
|
\post `this->size() = this->size() - std::min(this->size(), max_bytes)`
|
||||||
|
\post `if (this->size() <= max_bytes) this->data() = nullptr`
|
||||||
|
\return Number of bytes removed. */
|
||||||
|
std::size_t remove_prefix(std::size_t max_bytes) noexcept;
|
||||||
|
|
||||||
|
/*! "Take" bytes from the beginning of `this` slice.
|
||||||
|
|
||||||
|
\note May invalidate previously retrieved pointers.
|
||||||
|
\post `this->size() = this->size() - std::min(this->size(), max_bytes)`
|
||||||
|
\post `if (this->size() <= max_bytes) this->data() = nullptr`
|
||||||
|
\return Slice containing the bytes removed from `this` slice. */
|
||||||
|
byte_slice take_slice(std::size_t max_bytes) noexcept;
|
||||||
|
|
||||||
|
/*! Return a shallow (cheap) copy of a slice from `begin` and `end` offsets.
|
||||||
|
|
||||||
|
\throw std::out_of_range If `end < begin`.
|
||||||
|
\throw std::out_of_range If `size() < end`.
|
||||||
|
\return Slice starting at `data() + begin` of size `end - begin`. */
|
||||||
|
byte_slice get_slice(std::size_t begin, std::size_t end) const;
|
||||||
|
};
|
||||||
|
} // epee
|
||||||
|
|
@ -0,0 +1,209 @@
|
|||||||
|
// Copyright (c) 2019, 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.
|
||||||
|
|
||||||
|
#include <atomic>
|
||||||
|
#include <cstdlib>
|
||||||
|
#include <cstring>
|
||||||
|
#include <limits>
|
||||||
|
#include <stdexcept>
|
||||||
|
#include <utility>
|
||||||
|
|
||||||
|
#include "byte_slice.h"
|
||||||
|
|
||||||
|
namespace epee
|
||||||
|
{
|
||||||
|
struct byte_slice_data
|
||||||
|
{
|
||||||
|
byte_slice_data() noexcept
|
||||||
|
: ref_count(1)
|
||||||
|
{}
|
||||||
|
|
||||||
|
virtual ~byte_slice_data() noexcept
|
||||||
|
{}
|
||||||
|
|
||||||
|
std::atomic<std::size_t> ref_count;
|
||||||
|
};
|
||||||
|
|
||||||
|
void release_byte_slice::operator()(byte_slice_data* ptr) const noexcept
|
||||||
|
{
|
||||||
|
if (ptr && --(ptr->ref_count) == 0)
|
||||||
|
{
|
||||||
|
ptr->~byte_slice_data();
|
||||||
|
free(ptr);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
namespace
|
||||||
|
{
|
||||||
|
template<typename T>
|
||||||
|
struct adapted_byte_slice final : byte_slice_data
|
||||||
|
{
|
||||||
|
explicit adapted_byte_slice(T&& buffer)
|
||||||
|
: byte_slice_data(), buffer(std::move(buffer))
|
||||||
|
{}
|
||||||
|
|
||||||
|
virtual ~adapted_byte_slice() noexcept final override
|
||||||
|
{}
|
||||||
|
|
||||||
|
const T buffer;
|
||||||
|
};
|
||||||
|
|
||||||
|
// bytes "follow" this structure in memory slab
|
||||||
|
struct raw_byte_slice final : byte_slice_data
|
||||||
|
{
|
||||||
|
raw_byte_slice() noexcept
|
||||||
|
: byte_slice_data()
|
||||||
|
{}
|
||||||
|
|
||||||
|
virtual ~raw_byte_slice() noexcept final override
|
||||||
|
{}
|
||||||
|
};
|
||||||
|
|
||||||
|
/* This technique is not-standard, but allows for the reference count and
|
||||||
|
memory for the bytes (when given a list of spans) to be allocated in a
|
||||||
|
single call. In that situation, the dynamic sized bytes are after/behind
|
||||||
|
the raw_byte_slice class. The C runtime has to track the number of bytes
|
||||||
|
allocated regardless, so free'ing is relatively easy. */
|
||||||
|
|
||||||
|
template<typename T, typename... U>
|
||||||
|
std::unique_ptr<T, release_byte_slice> allocate_slice(std::size_t extra_bytes, U&&... args)
|
||||||
|
{
|
||||||
|
if (std::numeric_limits<std::size_t>::max() - sizeof(T) < extra_bytes)
|
||||||
|
throw std::bad_alloc{};
|
||||||
|
|
||||||
|
void* const ptr = malloc(sizeof(T) + extra_bytes);
|
||||||
|
if (ptr == nullptr)
|
||||||
|
throw std::bad_alloc{};
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
new (ptr) T{std::forward<U>(args)...};
|
||||||
|
}
|
||||||
|
catch (...)
|
||||||
|
{
|
||||||
|
free(ptr);
|
||||||
|
throw;
|
||||||
|
}
|
||||||
|
return std::unique_ptr<T, release_byte_slice>{reinterpret_cast<T*>(ptr)};
|
||||||
|
}
|
||||||
|
} // anonymous
|
||||||
|
|
||||||
|
byte_slice::byte_slice(byte_slice_data* storage, span<const std::uint8_t> portion) noexcept
|
||||||
|
: storage_(storage), portion_(portion)
|
||||||
|
{
|
||||||
|
if (storage_)
|
||||||
|
++(storage_->ref_count);
|
||||||
|
}
|
||||||
|
|
||||||
|
template<typename T>
|
||||||
|
byte_slice::byte_slice(const adapt_buffer, T&& buffer)
|
||||||
|
: storage_(nullptr), portion_(to_byte_span(to_span(buffer)))
|
||||||
|
{
|
||||||
|
if (!buffer.empty())
|
||||||
|
storage_ = allocate_slice<adapted_byte_slice<T>>(0, std::move(buffer));
|
||||||
|
}
|
||||||
|
|
||||||
|
byte_slice::byte_slice(std::initializer_list<span<const std::uint8_t>> sources)
|
||||||
|
: byte_slice()
|
||||||
|
{
|
||||||
|
std::size_t space_needed = 0;
|
||||||
|
for (const auto source : sources)
|
||||||
|
space_needed += source.size();
|
||||||
|
|
||||||
|
if (space_needed)
|
||||||
|
{
|
||||||
|
auto storage = allocate_slice<raw_byte_slice>(space_needed);
|
||||||
|
span<std::uint8_t> out{reinterpret_cast<std::uint8_t*>(storage.get() + 1), space_needed};
|
||||||
|
portion_ = {out.data(), out.size()};
|
||||||
|
|
||||||
|
for (const auto source : sources)
|
||||||
|
{
|
||||||
|
std::memcpy(out.data(), source.data(), source.size());
|
||||||
|
if (out.remove_prefix(source.size()) < source.size())
|
||||||
|
throw std::bad_alloc{}; // size_t overflow on space_needed
|
||||||
|
}
|
||||||
|
storage_ = std::move(storage);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
byte_slice::byte_slice(std::string&& buffer)
|
||||||
|
: byte_slice(adapt_buffer{}, std::move(buffer))
|
||||||
|
{}
|
||||||
|
|
||||||
|
byte_slice::byte_slice(std::vector<std::uint8_t>&& buffer)
|
||||||
|
: byte_slice(adapt_buffer{}, std::move(buffer))
|
||||||
|
{}
|
||||||
|
|
||||||
|
byte_slice::byte_slice(byte_slice&& source) noexcept
|
||||||
|
: storage_(std::move(source.storage_)), portion_(source.portion_)
|
||||||
|
{
|
||||||
|
source.portion_ = epee::span<const std::uint8_t>{};
|
||||||
|
}
|
||||||
|
|
||||||
|
byte_slice& byte_slice::operator=(byte_slice&& source) noexcept
|
||||||
|
{
|
||||||
|
storage_ = std::move(source.storage_);
|
||||||
|
portion_ = source.portion_;
|
||||||
|
if (source.storage_ == nullptr)
|
||||||
|
source.portion_ = epee::span<const std::uint8_t>{};
|
||||||
|
|
||||||
|
return *this;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::size_t byte_slice::remove_prefix(std::size_t max_bytes) noexcept
|
||||||
|
{
|
||||||
|
max_bytes = portion_.remove_prefix(max_bytes);
|
||||||
|
if (portion_.empty())
|
||||||
|
storage_ = nullptr;
|
||||||
|
return max_bytes;
|
||||||
|
}
|
||||||
|
|
||||||
|
byte_slice byte_slice::take_slice(const std::size_t max_bytes) noexcept
|
||||||
|
{
|
||||||
|
byte_slice out{};
|
||||||
|
std::uint8_t const* const ptr = data();
|
||||||
|
out.portion_ = {ptr, portion_.remove_prefix(max_bytes)};
|
||||||
|
|
||||||
|
if (portion_.empty())
|
||||||
|
out.storage_ = std::move(storage_); // no atomic inc/dec
|
||||||
|
else
|
||||||
|
out = {storage_.get(), out.portion_};
|
||||||
|
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
byte_slice byte_slice::get_slice(const std::size_t begin, const std::size_t end) const
|
||||||
|
{
|
||||||
|
if (end < begin || portion_.size() < end)
|
||||||
|
throw std::out_of_range{"bad slice range"};
|
||||||
|
|
||||||
|
if (begin == end)
|
||||||
|
return {};
|
||||||
|
return {storage_.get(), {portion_.begin() + begin, end - begin}};
|
||||||
|
}
|
||||||
|
} // epee
|
@ -0,0 +1,128 @@
|
|||||||
|
// Copyright (c) 2019, 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.
|
||||||
|
|
||||||
|
#include "net/levin_base.h"
|
||||||
|
|
||||||
|
#include "int-util.h"
|
||||||
|
|
||||||
|
namespace epee
|
||||||
|
{
|
||||||
|
namespace levin
|
||||||
|
{
|
||||||
|
bucket_head2 make_header(uint32_t command, uint64_t msg_size, uint32_t flags, bool expect_response) noexcept
|
||||||
|
{
|
||||||
|
bucket_head2 head = {0};
|
||||||
|
head.m_signature = SWAP64LE(LEVIN_SIGNATURE);
|
||||||
|
head.m_have_to_return_data = expect_response;
|
||||||
|
head.m_cb = SWAP64LE(msg_size);
|
||||||
|
|
||||||
|
head.m_command = SWAP32LE(command);
|
||||||
|
head.m_protocol_version = SWAP32LE(LEVIN_PROTOCOL_VER_1);
|
||||||
|
head.m_flags = SWAP32LE(flags);
|
||||||
|
return head;
|
||||||
|
}
|
||||||
|
|
||||||
|
byte_slice make_notify(int command, epee::span<const std::uint8_t> payload)
|
||||||
|
{
|
||||||
|
const bucket_head2 head = make_header(command, payload.size(), LEVIN_PACKET_REQUEST, false);
|
||||||
|
return byte_slice{epee::as_byte_span(head), payload};
|
||||||
|
}
|
||||||
|
|
||||||
|
byte_slice make_noise_notify(const std::size_t noise_bytes)
|
||||||
|
{
|
||||||
|
static constexpr const std::uint32_t flags =
|
||||||
|
LEVIN_PACKET_BEGIN | LEVIN_PACKET_END;
|
||||||
|
|
||||||
|
if (noise_bytes < sizeof(bucket_head2))
|
||||||
|
return nullptr;
|
||||||
|
|
||||||
|
std::string buffer(noise_bytes, char(0));
|
||||||
|
const bucket_head2 head = make_header(0, noise_bytes - sizeof(bucket_head2), flags, false);
|
||||||
|
std::memcpy(std::addressof(buffer[0]), std::addressof(head), sizeof(head));
|
||||||
|
|
||||||
|
return byte_slice{std::move(buffer)};
|
||||||
|
}
|
||||||
|
|
||||||
|
byte_slice make_fragmented_notify(const byte_slice& noise_message, int command, epee::span<const std::uint8_t> payload)
|
||||||
|
{
|
||||||
|
const size_t noise_size = noise_message.size();
|
||||||
|
if (noise_size < sizeof(bucket_head2) * 2)
|
||||||
|
return nullptr;
|
||||||
|
|
||||||
|
if (payload.size() <= noise_size - sizeof(bucket_head2))
|
||||||
|
{
|
||||||
|
/* The entire message can be sent at once, and the levin binary parser
|
||||||
|
will ignore extra bytes. So just pad with zeroes and otherwise send
|
||||||
|
a "normal", not fragmented message. */
|
||||||
|
const size_t padding = noise_size - sizeof(bucket_head2) - payload.size();
|
||||||
|
const span<const uint8_t> padding_bytes{noise_message.end() - padding, padding};
|
||||||
|
|
||||||
|
const bucket_head2 head = make_header(command, noise_size - sizeof(bucket_head2), LEVIN_PACKET_REQUEST, false);
|
||||||
|
return byte_slice{as_byte_span(head), payload, padding_bytes};
|
||||||
|
}
|
||||||
|
|
||||||
|
// fragment message
|
||||||
|
const size_t payload_space = noise_size - sizeof(bucket_head2);
|
||||||
|
const size_t expected_fragments = ((payload.size() - 2) / payload_space) + 1;
|
||||||
|
|
||||||
|
std::string buffer{};
|
||||||
|
buffer.reserve((expected_fragments + 1) * noise_size); // +1 here overselects for internal bucket_head2 value
|
||||||
|
|
||||||
|
bucket_head2 head = make_header(0, noise_size - sizeof(bucket_head2), LEVIN_PACKET_BEGIN, false);
|
||||||
|
buffer.append(reinterpret_cast<const char*>(&head), sizeof(head));
|
||||||
|
|
||||||
|
head.m_command = command;
|
||||||
|
head.m_flags = LEVIN_PACKET_REQUEST;
|
||||||
|
head.m_cb = payload.size();
|
||||||
|
buffer.append(reinterpret_cast<const char*>(&head), sizeof(head));
|
||||||
|
|
||||||
|
size_t copy_size = payload.remove_prefix(payload_space - sizeof(bucket_head2));
|
||||||
|
buffer.append(reinterpret_cast<const char*>(payload.data()) - copy_size, copy_size);
|
||||||
|
|
||||||
|
head.m_command = 0;
|
||||||
|
head.m_flags = 0;
|
||||||
|
head.m_cb = noise_size - sizeof(bucket_head2);
|
||||||
|
|
||||||
|
while (!payload.empty())
|
||||||
|
{
|
||||||
|
copy_size = payload.remove_prefix(payload_space);
|
||||||
|
|
||||||
|
if (payload.empty())
|
||||||
|
head.m_flags = LEVIN_PACKET_END;
|
||||||
|
|
||||||
|
buffer.append(reinterpret_cast<const char*>(&head), sizeof(head));
|
||||||
|
buffer.append(reinterpret_cast<const char*>(payload.data()) - copy_size, copy_size);
|
||||||
|
}
|
||||||
|
|
||||||
|
const size_t padding = noise_size - copy_size - sizeof(bucket_head2);
|
||||||
|
buffer.append(reinterpret_cast<const char*>(noise_message.end()) - padding, padding);
|
||||||
|
|
||||||
|
return byte_slice{std::move(buffer)};
|
||||||
|
}
|
||||||
|
} // levin
|
||||||
|
} // epee
|
@ -0,0 +1,574 @@
|
|||||||
|
// Copyright (c) 2019, 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.
|
||||||
|
|
||||||
|
#include "levin_notify.h"
|
||||||
|
|
||||||
|
#include <boost/asio/steady_timer.hpp>
|
||||||
|
#include <boost/system/system_error.hpp>
|
||||||
|
#include <chrono>
|
||||||
|
#include <deque>
|
||||||
|
#include <stdexcept>
|
||||||
|
|
||||||
|
#include "common/expect.h"
|
||||||
|
#include "common/varint.h"
|
||||||
|
#include "cryptonote_config.h"
|
||||||
|
#include "crypto/random.h"
|
||||||
|
#include "cryptonote_basic/connection_context.h"
|
||||||
|
#include "cryptonote_protocol/cryptonote_protocol_defs.h"
|
||||||
|
#include "net/dandelionpp.h"
|
||||||
|
#include "p2p/net_node.h"
|
||||||
|
|
||||||
|
namespace cryptonote
|
||||||
|
{
|
||||||
|
namespace levin
|
||||||
|
{
|
||||||
|
namespace
|
||||||
|
{
|
||||||
|
constexpr 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::seconds noise_min_delay{CRYPTONOTE_NOISE_MIN_DELAY};
|
||||||
|
constexpr const std::chrono::seconds noise_delay_range{CRYPTONOTE_NOISE_DELAY_RANGE};
|
||||||
|
|
||||||
|
/*! 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
|
||||||
|
function will select a duration from [0, 3] seconds, and the increments
|
||||||
|
for the selection will be determined by the `steady_clock` precision
|
||||||
|
(typically nanoseconds).
|
||||||
|
|
||||||
|
\return A randomized duration from 0 to `range`. */
|
||||||
|
std::chrono::steady_clock::duration random_duration(std::chrono::steady_clock::duration range)
|
||||||
|
{
|
||||||
|
using rep = std::chrono::steady_clock::rep;
|
||||||
|
return std::chrono::steady_clock::duration{crypto::rand_range(rep(0), range.count())};
|
||||||
|
}
|
||||||
|
|
||||||
|
//! \return All outgoing connections supporting fragments in `connections`.
|
||||||
|
std::vector<boost::uuids::uuid> get_out_connections(connections& p2p)
|
||||||
|
{
|
||||||
|
std::vector<boost::uuids::uuid> outs;
|
||||||
|
outs.reserve(connection_id_reserve_size);
|
||||||
|
|
||||||
|
/* The foreach call is serialized with a lock, but should be quick due to
|
||||||
|
the reserve call so a strand is not used. Investigate if there is lots
|
||||||
|
of waiting in here. */
|
||||||
|
|
||||||
|
p2p.foreach_connection([&outs] (detail::p2p_context& context) {
|
||||||
|
if (!context.m_is_income)
|
||||||
|
outs.emplace_back(context.m_connection_id);
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
|
||||||
|
return outs;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string make_tx_payload(std::vector<blobdata>&& txs, const bool pad)
|
||||||
|
{
|
||||||
|
NOTIFY_NEW_TRANSACTIONS::request request{};
|
||||||
|
request.txs = std::move(txs);
|
||||||
|
|
||||||
|
if (pad)
|
||||||
|
{
|
||||||
|
size_t bytes = 9 /* header */ + 4 /* 1 + 'txs' */ + tools::get_varint_data(request.txs.size()).size();
|
||||||
|
for(auto tx_blob_it = request.txs.begin(); tx_blob_it!=request.txs.end(); ++tx_blob_it)
|
||||||
|
bytes += tools::get_varint_data(tx_blob_it->size()).size() + tx_blob_it->size();
|
||||||
|
|
||||||
|
// stuff some dummy bytes in to stay safe from traffic volume analysis
|
||||||
|
static constexpr const size_t granularity = 1024;
|
||||||
|
size_t padding = granularity - bytes % granularity;
|
||||||
|
const size_t overhead = 2 /* 1 + '_' */ + tools::get_varint_data(padding).size();
|
||||||
|
if (overhead > padding)
|
||||||
|
padding = 0;
|
||||||
|
else
|
||||||
|
padding -= overhead;
|
||||||
|
request._ = std::string(padding, ' ');
|
||||||
|
|
||||||
|
std::string arg_buff;
|
||||||
|
epee::serialization::store_t_to_binary(request, arg_buff);
|
||||||
|
|
||||||
|
// we probably lowballed the payload size a bit, so added a but too much. Fix this now.
|
||||||
|
size_t remove = arg_buff.size() % granularity;
|
||||||
|
if (remove > request._.size())
|
||||||
|
request._.clear();
|
||||||
|
else
|
||||||
|
request._.resize(request._.size() - remove);
|
||||||
|
// if the size of _ moved enough, we might lose byte in size encoding, we don't care
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string fullBlob;
|
||||||
|
if (!epee::serialization::store_t_to_binary(request, fullBlob))
|
||||||
|
throw std::runtime_error{"Failed to serialize to epee binary format"};
|
||||||
|
|
||||||
|
return fullBlob;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* The current design uses `asio::strand`s. The documentation isn't as clear
|
||||||
|
as it should be - a `strand` has an internal `mutex` and `bool`. The
|
||||||
|
`mutex` synchronizes thread access and the `bool` is set when a thread is
|
||||||
|
executing something "in the strand". Therefore, if a callback has lots of
|
||||||
|
work to do in a `strand`, asio can switch to some other task instead of
|
||||||
|
blocking 1+ threads to wait for the original thread to complete the task
|
||||||
|
(as is the case when client code has a `mutex` inside the callback). The
|
||||||
|
downside is that asio _always_ allocates for the callback, even if it can
|
||||||
|
be immediately executed. So if all work in a strand is minimal, a lock
|
||||||
|
may be better.
|
||||||
|
|
||||||
|
This code uses a strand per "zone" and a strand per "channel in a zone".
|
||||||
|
`dispatch` is used heavily, which means "execute immediately in _this_
|
||||||
|
thread if the strand is not in use, otherwise queue the callback to be
|
||||||
|
executed immediately after the strand completes its current task".
|
||||||
|
`post` is used where deferred execution to an `asio::io_service::run`
|
||||||
|
thread is preferred.
|
||||||
|
|
||||||
|
The strand per "zone" is useful because the levin
|
||||||
|
`foreach_connection` is blocked with a mutex anyway. So this primarily
|
||||||
|
helps with reducing blocking of a thread attempting a "flood"
|
||||||
|
notification. Updating/merging the outgoing connections in the
|
||||||
|
Dandelion++ map is also somewhat expensive.
|
||||||
|
|
||||||
|
The strand per "channel" may need a re-visit. The most "expensive" code
|
||||||
|
is figuring out the noise/notification to send. If levin code is
|
||||||
|
optimized further, it might be better to just use standard locks per
|
||||||
|
channel. */
|
||||||
|
|
||||||
|
//! A queue of levin messages for a noise i2p/tor link
|
||||||
|
struct noise_channel
|
||||||
|
{
|
||||||
|
explicit noise_channel(boost::asio::io_service& io_service)
|
||||||
|
: active(nullptr),
|
||||||
|
queue(),
|
||||||
|
strand(io_service),
|
||||||
|
next_noise(io_service),
|
||||||
|
connection(boost::uuids::nil_uuid())
|
||||||
|
{}
|
||||||
|
|
||||||
|
// `asio::io_service::strand` cannot be copied or moved
|
||||||
|
noise_channel(const noise_channel&) = delete;
|
||||||
|
noise_channel& operator=(const noise_channel&) = delete;
|
||||||
|
|
||||||
|
// Only read/write these values "inside the strand"
|
||||||
|
|
||||||
|
epee::byte_slice active;
|
||||||
|
std::deque<epee::byte_slice> queue;
|
||||||
|
boost::asio::io_service::strand strand;
|
||||||
|
boost::asio::steady_timer next_noise;
|
||||||
|
boost::uuids::uuid connection;
|
||||||
|
};
|
||||||
|
} // anonymous
|
||||||
|
|
||||||
|
namespace detail
|
||||||
|
{
|
||||||
|
struct zone
|
||||||
|
{
|
||||||
|
explicit zone(boost::asio::io_service& io_service, std::shared_ptr<connections> p2p, epee::byte_slice noise_in)
|
||||||
|
: p2p(std::move(p2p)),
|
||||||
|
noise(std::move(noise_in)),
|
||||||
|
next_epoch(io_service),
|
||||||
|
strand(io_service),
|
||||||
|
map(),
|
||||||
|
channels(),
|
||||||
|
connection_count(0)
|
||||||
|
{
|
||||||
|
for (std::size_t count = 0; !noise.empty() && count < CRYPTONOTE_NOISE_CHANNELS; ++count)
|
||||||
|
channels.emplace_back(io_service);
|
||||||
|
}
|
||||||
|
|
||||||
|
const std::shared_ptr<connections> p2p;
|
||||||
|
const epee::byte_slice noise; //!< `!empty()` means zone is using noise channels
|
||||||
|
boost::asio::steady_timer next_epoch;
|
||||||
|
boost::asio::io_service::strand strand;
|
||||||
|
net::dandelionpp::connection_map map;//!< Tracks outgoing uuid's for noise channels or Dandelion++ stems
|
||||||
|
std::deque<noise_channel> channels; //!< Never touch after init; only update elements on `noise_channel.strand`
|
||||||
|
std::atomic<std::size_t> connection_count; //!< Only update in strand, can be read at any time
|
||||||
|
};
|
||||||
|
} // detail
|
||||||
|
|
||||||
|
namespace
|
||||||
|
{
|
||||||
|
//! Adds a message to the sending queue of the channel.
|
||||||
|
class queue_covert_notify
|
||||||
|
{
|
||||||
|
std::shared_ptr<detail::zone> zone_;
|
||||||
|
epee::byte_slice message_; // Requires manual copy constructor
|
||||||
|
const std::size_t destination_;
|
||||||
|
|
||||||
|
public:
|
||||||
|
queue_covert_notify(std::shared_ptr<detail::zone> zone, epee::byte_slice message, std::size_t destination)
|
||||||
|
: zone_(std::move(zone)), message_(std::move(message)), destination_(destination)
|
||||||
|
{}
|
||||||
|
|
||||||
|
queue_covert_notify(queue_covert_notify&&) = default;
|
||||||
|
queue_covert_notify(const queue_covert_notify& source)
|
||||||
|
: zone_(source.zone_), message_(source.message_.clone()), destination_(source.destination_)
|
||||||
|
{}
|
||||||
|
|
||||||
|
//! \pre Called within `zone_->channels[destionation_].strand`.
|
||||||
|
void operator()()
|
||||||
|
{
|
||||||
|
if (!zone_)
|
||||||
|
return;
|
||||||
|
|
||||||
|
noise_channel& channel = zone_->channels.at(destination_);
|
||||||
|
assert(channel.strand.running_in_this_thread());
|
||||||
|
|
||||||
|
if (!channel.connection.is_nil())
|
||||||
|
channel.queue.push_back(std::move(message_));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
//! Sends a message to every active connection
|
||||||
|
class flood_notify
|
||||||
|
{
|
||||||
|
std::shared_ptr<detail::zone> zone_;
|
||||||
|
epee::byte_slice message_; // Requires manual copy
|
||||||
|
boost::uuids::uuid source_;
|
||||||
|
|
||||||
|
public:
|
||||||
|
explicit flood_notify(std::shared_ptr<detail::zone> zone, epee::byte_slice message, const boost::uuids::uuid& source)
|
||||||
|
: zone_(std::move(zone)), message_(message.clone()), source_(source)
|
||||||
|
{}
|
||||||
|
|
||||||
|
flood_notify(flood_notify&&) = default;
|
||||||
|
flood_notify(const flood_notify& source)
|
||||||
|
: zone_(source.zone_), message_(source.message_.clone()), source_(source.source_)
|
||||||
|
{}
|
||||||
|
|
||||||
|
void operator()() const
|
||||||
|
{
|
||||||
|
if (!zone_ || !zone_->p2p)
|
||||||
|
return;
|
||||||
|
|
||||||
|
assert(zone_->strand.running_in_this_thread());
|
||||||
|
|
||||||
|
/* The foreach should be quick, but then it iterates and acquires the
|
||||||
|
same lock for every connection. So do in a strand because two threads
|
||||||
|
will ping-pong each other with cacheline invalidations. Revisit if
|
||||||
|
algorithm changes or the locking strategy within the levin config
|
||||||
|
class changes. */
|
||||||
|
|
||||||
|
std::vector<boost::uuids::uuid> connections;
|
||||||
|
connections.reserve(connection_id_reserve_size);
|
||||||
|
zone_->p2p->foreach_connection([this, &connections] (detail::p2p_context& context) {
|
||||||
|
if (this->source_ != context.m_connection_id)
|
||||||
|
connections.emplace_back(context.m_connection_id);
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
|
||||||
|
for (const boost::uuids::uuid& connection : connections)
|
||||||
|
zone_->p2p->send(message_.clone(), connection);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
//! Updates the connection for a channel.
|
||||||
|
struct update_channel
|
||||||
|
{
|
||||||
|
std::shared_ptr<detail::zone> zone_;
|
||||||
|
const std::size_t channel_;
|
||||||
|
const boost::uuids::uuid connection_;
|
||||||
|
|
||||||
|
//! \pre Called within `stem_.strand`.
|
||||||
|
void operator()() const
|
||||||
|
{
|
||||||
|
if (!zone_)
|
||||||
|
return;
|
||||||
|
|
||||||
|
noise_channel& channel = zone_->channels.at(channel_);
|
||||||
|
assert(channel.strand.running_in_this_thread());
|
||||||
|
static_assert(
|
||||||
|
CRYPTONOTE_MAX_FRAGMENTS <= (noise_min_epoch / (noise_min_delay + noise_delay_range)),
|
||||||
|
"Max fragments more than the max that can be sent in an epoch"
|
||||||
|
);
|
||||||
|
|
||||||
|
/* This clears the active message so that a message "in-flight" is
|
||||||
|
restarted. DO NOT try to send the remainder of the fragments, this
|
||||||
|
additional send time can leak that this node was sending out a real
|
||||||
|
notify (tx) instead of dummy noise. */
|
||||||
|
|
||||||
|
channel.connection = connection_;
|
||||||
|
channel.active = nullptr;
|
||||||
|
|
||||||
|
if (connection_.is_nil())
|
||||||
|
channel.queue.clear();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
//! Merges `out_connections_` into the existing `zone_->map`.
|
||||||
|
struct update_channels
|
||||||
|
{
|
||||||
|
std::shared_ptr<detail::zone> zone_;
|
||||||
|
std::vector<boost::uuids::uuid> out_connections_;
|
||||||
|
|
||||||
|
//! \pre Called within `zone->strand`.
|
||||||
|
static void post(std::shared_ptr<detail::zone> zone)
|
||||||
|
{
|
||||||
|
if (!zone)
|
||||||
|
return;
|
||||||
|
|
||||||
|
assert(zone->strand.running_in_this_thread());
|
||||||
|
|
||||||
|
zone->connection_count = zone->map.size();
|
||||||
|
for (auto id = zone->map.begin(); id != zone->map.end(); ++id)
|
||||||
|
{
|
||||||
|
const std::size_t i = id - zone->map.begin();
|
||||||
|
zone->channels[i].strand.post(update_channel{zone, i, *id});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//! \pre Called within `zone_->strand`.
|
||||||
|
void operator()()
|
||||||
|
{
|
||||||
|
if (!zone_)
|
||||||
|
return;
|
||||||
|
|
||||||
|
assert(zone_->strand.running_in_this_thread());
|
||||||
|
if (zone_->map.update(std::move(out_connections_)))
|
||||||
|
post(std::move(zone_));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
//! Swaps out noise channels entirely; new epoch start.
|
||||||
|
class change_channels
|
||||||
|
{
|
||||||
|
std::shared_ptr<detail::zone> zone_;
|
||||||
|
net::dandelionpp::connection_map map_; // Requires manual copy constructor
|
||||||
|
|
||||||
|
public:
|
||||||
|
explicit change_channels(std::shared_ptr<detail::zone> zone, net::dandelionpp::connection_map map)
|
||||||
|
: zone_(std::move(zone)), map_(std::move(map))
|
||||||
|
{}
|
||||||
|
|
||||||
|
change_channels(change_channels&&) = default;
|
||||||
|
change_channels(const change_channels& source)
|
||||||
|
: zone_(source.zone_), map_(source.map_.clone())
|
||||||
|
{}
|
||||||
|
|
||||||
|
//! \pre Called within `zone_->strand`.
|
||||||
|
void operator()()
|
||||||
|
{
|
||||||
|
if (!zone_)
|
||||||
|
return
|
||||||
|
|
||||||
|
assert(zone_->strand.running_in_this_thread());
|
||||||
|
|
||||||
|
zone_->map = std::move(map_);
|
||||||
|
update_channels::post(std::move(zone_));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
//! Sends a noise packet or real notification and sets timer for next call.
|
||||||
|
struct send_noise
|
||||||
|
{
|
||||||
|
std::shared_ptr<detail::zone> zone_;
|
||||||
|
const std::size_t channel_;
|
||||||
|
|
||||||
|
static void wait(const std::chrono::steady_clock::time_point start, std::shared_ptr<detail::zone> zone, const std::size_t index)
|
||||||
|
{
|
||||||
|
if (!zone)
|
||||||
|
return;
|
||||||
|
|
||||||
|
noise_channel& channel = zone->channels.at(index);
|
||||||
|
channel.next_noise.expires_at(start + noise_min_delay + random_duration(noise_delay_range));
|
||||||
|
channel.next_noise.async_wait(
|
||||||
|
channel.strand.wrap(send_noise{std::move(zone), index})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
//! \pre Called within `zone_->channels[channel_].strand`.
|
||||||
|
void operator()(boost::system::error_code error)
|
||||||
|
{
|
||||||
|
if (!zone_ || !zone_->p2p || zone_->noise.empty())
|
||||||
|
return;
|
||||||
|
|
||||||
|
if (error && error != boost::system::errc::operation_canceled)
|
||||||
|
throw boost::system::system_error{error, "send_noise timer failed"};
|
||||||
|
|
||||||
|
assert(zone_->channels.at(channel_).strand.running_in_this_thread());
|
||||||
|
|
||||||
|
const auto start = std::chrono::steady_clock::now();
|
||||||
|
noise_channel& channel = zone_->channels.at(channel_);
|
||||||
|
|
||||||
|
if (!channel.connection.is_nil())
|
||||||
|
{
|
||||||
|
epee::byte_slice message = nullptr;
|
||||||
|
if (!channel.active.empty())
|
||||||
|
message = channel.active.take_slice(zone_->noise.size());
|
||||||
|
else if (!channel.queue.empty())
|
||||||
|
{
|
||||||
|
channel.active = channel.queue.front().clone();
|
||||||
|
message = channel.active.take_slice(zone_->noise.size());
|
||||||
|
}
|
||||||
|
else
|
||||||
|
message = zone_->noise.clone();
|
||||||
|
|
||||||
|
if (zone_->p2p->send(std::move(message), channel.connection))
|
||||||
|
{
|
||||||
|
if (!channel.queue.empty() && channel.active.empty())
|
||||||
|
channel.queue.pop_front();
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
channel.active = nullptr;
|
||||||
|
channel.connection = boost::uuids::nil_uuid();
|
||||||
|
zone_->strand.post(
|
||||||
|
update_channels{zone_, get_out_connections(*zone_->p2p)}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
wait(start, std::move(zone_), channel_);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
//! Prepares connections for new channel epoch and sets timer for next epoch
|
||||||
|
struct start_epoch
|
||||||
|
{
|
||||||
|
// Variables allow for Dandelion++ extension
|
||||||
|
std::shared_ptr<detail::zone> zone_;
|
||||||
|
std::chrono::seconds min_epoch_;
|
||||||
|
std::chrono::seconds epoch_range_;
|
||||||
|
std::size_t count_;
|
||||||
|
|
||||||
|
//! \pre Should not be invoked within any strand to prevent blocking.
|
||||||
|
void operator()(const boost::system::error_code error = {})
|
||||||
|
{
|
||||||
|
if (!zone_ || !zone_->p2p)
|
||||||
|
return;
|
||||||
|
|
||||||
|
if (error && error != boost::system::errc::operation_canceled)
|
||||||
|
throw boost::system::system_error{error, "start_epoch timer failed"};
|
||||||
|
|
||||||
|
const auto start = std::chrono::steady_clock::now();
|
||||||
|
zone_->strand.dispatch(
|
||||||
|
change_channels{zone_, net::dandelionpp::connection_map{get_out_connections(*(zone_->p2p)), count_}}
|
||||||
|
);
|
||||||
|
|
||||||
|
detail::zone& alias = *zone_;
|
||||||
|
alias.next_epoch.expires_at(start + min_epoch_ + random_duration(epoch_range_));
|
||||||
|
alias.next_epoch.async_wait(start_epoch{std::move(*this)});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
} // anonymous
|
||||||
|
|
||||||
|
notify::notify(boost::asio::io_service& service, std::shared_ptr<connections> p2p, epee::byte_slice noise)
|
||||||
|
: zone_(std::make_shared<detail::zone>(service, std::move(p2p), std::move(noise)))
|
||||||
|
{
|
||||||
|
if (!zone_->p2p)
|
||||||
|
throw std::logic_error{"cryptonote::levin::notify cannot have nullptr p2p argument"};
|
||||||
|
|
||||||
|
if (!zone_->noise.empty())
|
||||||
|
{
|
||||||
|
const auto now = std::chrono::steady_clock::now();
|
||||||
|
start_epoch{zone_, noise_min_epoch, noise_epoch_range, CRYPTONOTE_NOISE_CHANNELS}();
|
||||||
|
for (std::size_t channel = 0; channel < zone_->channels.size(); ++channel)
|
||||||
|
send_noise::wait(now, zone_, channel);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
notify::~notify() noexcept
|
||||||
|
{}
|
||||||
|
|
||||||
|
notify::status notify::get_status() const noexcept
|
||||||
|
{
|
||||||
|
if (!zone_)
|
||||||
|
return {false, false};
|
||||||
|
|
||||||
|
return {!zone_->noise.empty(), CRYPTONOTE_NOISE_CHANNELS <= zone_->connection_count};
|
||||||
|
}
|
||||||
|
|
||||||
|
void notify::new_out_connection()
|
||||||
|
{
|
||||||
|
if (!zone_ || zone_->noise.empty() || CRYPTONOTE_NOISE_CHANNELS <= zone_->connection_count)
|
||||||
|
return;
|
||||||
|
|
||||||
|
zone_->strand.dispatch(
|
||||||
|
update_channels{zone_, get_out_connections(*(zone_->p2p))}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
void notify::run_epoch()
|
||||||
|
{
|
||||||
|
if (!zone_)
|
||||||
|
return;
|
||||||
|
zone_->next_epoch.cancel();
|
||||||
|
}
|
||||||
|
|
||||||
|
void notify::run_stems()
|
||||||
|
{
|
||||||
|
if (!zone_)
|
||||||
|
return;
|
||||||
|
|
||||||
|
for (noise_channel& channel : zone_->channels)
|
||||||
|
channel.next_noise.cancel();
|
||||||
|
}
|
||||||
|
|
||||||
|
bool notify::send_txs(std::vector<cryptonote::blobdata> txs, const boost::uuids::uuid& source, const bool pad_txs)
|
||||||
|
{
|
||||||
|
if (!zone_)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
if (!zone_->noise.empty() && !zone_->channels.empty())
|
||||||
|
{
|
||||||
|
// covert send in "noise" channel
|
||||||
|
static_assert(
|
||||||
|
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);
|
||||||
|
epee::byte_slice message = epee::levin::make_fragmented_notify(
|
||||||
|
zone_->noise, NOTIFY_NEW_TRANSACTIONS::ID, epee::strspan<std::uint8_t>(payload)
|
||||||
|
);
|
||||||
|
if (CRYPTONOTE_MAX_FRAGMENTS * zone_->noise.size() < message.size())
|
||||||
|
{
|
||||||
|
MERROR("notify::send_txs provided message exceeding covert fragment size");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (std::size_t channel = 0; channel < zone_->channels.size(); ++channel)
|
||||||
|
{
|
||||||
|
zone_->channels[channel].strand.dispatch(
|
||||||
|
queue_covert_notify{zone_, message.clone(), channel}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
const std::string payload = make_tx_payload(std::move(txs), pad_txs);
|
||||||
|
epee::byte_slice message =
|
||||||
|
epee::levin::make_notify(NOTIFY_NEW_TRANSACTIONS::ID, epee::strspan<std::uint8_t>(payload));
|
||||||
|
|
||||||
|
// traditional monero send technique
|
||||||
|
zone_->strand.dispatch(flood_notify{zone_, std::move(message), source});
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
} // levin
|
||||||
|
} // net
|
@ -0,0 +1,132 @@
|
|||||||
|
// Copyright (c) 2019, 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 <boost/asio/io_service.hpp>
|
||||||
|
#include <boost/uuid/uuid.hpp>
|
||||||
|
#include <memory>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
#include "byte_slice.h"
|
||||||
|
#include "cryptonote_basic/blobdatatype.h"
|
||||||
|
#include "net/enums.h"
|
||||||
|
#include "span.h"
|
||||||
|
|
||||||
|
namespace epee
|
||||||
|
{
|
||||||
|
namespace levin
|
||||||
|
{
|
||||||
|
template<typename> class async_protocol_handler_config;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
namespace nodetool
|
||||||
|
{
|
||||||
|
template<typename> struct p2p_connection_context_t;
|
||||||
|
}
|
||||||
|
|
||||||
|
namespace cryptonote
|
||||||
|
{
|
||||||
|
struct cryptonote_connection_context;
|
||||||
|
}
|
||||||
|
|
||||||
|
namespace cryptonote
|
||||||
|
{
|
||||||
|
namespace levin
|
||||||
|
{
|
||||||
|
namespace detail
|
||||||
|
{
|
||||||
|
using p2p_context = nodetool::p2p_connection_context_t<cryptonote::cryptonote_connection_context>;
|
||||||
|
struct zone; //!< Internal data needed for zone notifications
|
||||||
|
} // detail
|
||||||
|
|
||||||
|
using connections = epee::levin::async_protocol_handler_config<detail::p2p_context>;
|
||||||
|
|
||||||
|
//! Provides tx notification privacy
|
||||||
|
class notify
|
||||||
|
{
|
||||||
|
std::shared_ptr<detail::zone> zone_;
|
||||||
|
|
||||||
|
public:
|
||||||
|
struct status
|
||||||
|
{
|
||||||
|
bool has_noise;
|
||||||
|
bool connections_filled;
|
||||||
|
};
|
||||||
|
|
||||||
|
//! Construct an instance that cannot notify.
|
||||||
|
notify() noexcept
|
||||||
|
: zone_(nullptr)
|
||||||
|
{}
|
||||||
|
|
||||||
|
//! Construct an instance with available notification `zones`.
|
||||||
|
explicit notify(boost::asio::io_service& service, std::shared_ptr<connections> p2p, epee::byte_slice noise);
|
||||||
|
|
||||||
|
notify(const notify&) = delete;
|
||||||
|
notify(notify&&) = default;
|
||||||
|
|
||||||
|
~notify() noexcept;
|
||||||
|
|
||||||
|
notify& operator=(const notify&) = delete;
|
||||||
|
notify& operator=(notify&&) = default;
|
||||||
|
|
||||||
|
//! \return Status information for zone selection.
|
||||||
|
status get_status() const noexcept;
|
||||||
|
|
||||||
|
//! Probe for new outbound connection - skips if not needed.
|
||||||
|
void new_out_connection();
|
||||||
|
|
||||||
|
//! Run the logic for the next epoch immediately. Only use in testing.
|
||||||
|
void run_epoch();
|
||||||
|
|
||||||
|
//! Run the logic for the next stem timeout imemdiately. Only use in testing.
|
||||||
|
void run_stems();
|
||||||
|
|
||||||
|
/*! Send txs using `cryptonote_protocol_defs.h` payload format wrapped in a
|
||||||
|
levin header. The message will be sent in a "discreet" manner if
|
||||||
|
enabled - if `!noise.empty()` then the `command`/`payload` will be
|
||||||
|
queued to send at the next available noise interval. Otherwise, a
|
||||||
|
standard Monero flood notification will be used.
|
||||||
|
|
||||||
|
\note Eventually Dandelion++ stem sending will be used here when
|
||||||
|
enabled.
|
||||||
|
|
||||||
|
\param txs The transactions that need to be serialized and relayed.
|
||||||
|
\param source The source of the notification. `is_nil()` indicates this
|
||||||
|
node is the source. Dandelion++ will use this to map a source to a
|
||||||
|
particular stem.
|
||||||
|
\param pad_txs A request to pad txs to help conceal origin via
|
||||||
|
statistical analysis. Ignored if noise was enabled during
|
||||||
|
construction.
|
||||||
|
|
||||||
|
\return True iff the notification is queued for sending. */
|
||||||
|
bool send_txs(std::vector<blobdata> txs, const boost::uuids::uuid& source, bool pad_txs);
|
||||||
|
};
|
||||||
|
} // levin
|
||||||
|
} // net
|
@ -0,0 +1,212 @@
|
|||||||
|
// Copyright (c) 2019, 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.
|
||||||
|
|
||||||
|
#include "dandelionpp.h"
|
||||||
|
|
||||||
|
#include <boost/container/small_vector.hpp>
|
||||||
|
#include <boost/uuid/nil_generator.hpp>
|
||||||
|
#include <chrono>
|
||||||
|
|
||||||
|
#include "common/expect.h"
|
||||||
|
#include "cryptonote_config.h"
|
||||||
|
#include "crypto/crypto.h"
|
||||||
|
|
||||||
|
namespace net
|
||||||
|
{
|
||||||
|
namespace dandelionpp
|
||||||
|
{
|
||||||
|
namespace
|
||||||
|
{
|
||||||
|
constexpr const std::size_t expected_max_channels = CRYPTONOTE_NOISE_CHANNELS;
|
||||||
|
|
||||||
|
// could be in util somewhere
|
||||||
|
struct key_less
|
||||||
|
{
|
||||||
|
template<typename K, typename V>
|
||||||
|
bool operator()(const std::pair<K, V>& left, const K& right) const
|
||||||
|
{
|
||||||
|
return left.first < right;
|
||||||
|
}
|
||||||
|
|
||||||
|
template<typename K, typename V>
|
||||||
|
bool operator()(const K& left, const std::pair<K, V>& right) const
|
||||||
|
{
|
||||||
|
return left < right.first;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
std::size_t select_stem(epee::span<const std::size_t> usage, epee::span<const boost::uuids::uuid> out_map)
|
||||||
|
{
|
||||||
|
assert(usage.size() < std::numeric_limits<std::size_t>::max()); // prevented in constructor
|
||||||
|
if (usage.size() < out_map.size())
|
||||||
|
return std::numeric_limits<std::size_t>::max();
|
||||||
|
|
||||||
|
// small_vector uses stack space if `expected_max_channels < capacity()`
|
||||||
|
std::size_t lowest = std::numeric_limits<std::size_t>::max();
|
||||||
|
boost::container::small_vector<std::size_t, expected_max_channels> choices;
|
||||||
|
static_assert(sizeof(choices) < 256, "choices is too large based on current configuration");
|
||||||
|
|
||||||
|
for (const boost::uuids::uuid& out : out_map)
|
||||||
|
{
|
||||||
|
if (!out.is_nil())
|
||||||
|
{
|
||||||
|
const std::size_t location = std::addressof(out) - out_map.begin();
|
||||||
|
if (usage[location] < lowest)
|
||||||
|
{
|
||||||
|
lowest = usage[location];
|
||||||
|
choices = {location};
|
||||||
|
}
|
||||||
|
else if (usage[location] == lowest)
|
||||||
|
choices.push_back(location);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
switch (choices.size())
|
||||||
|
{
|
||||||
|
case 0:
|
||||||
|
return std::numeric_limits<std::size_t>::max();
|
||||||
|
case 1:
|
||||||
|
return choices[0];
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
return choices[crypto::rand_idx(choices.size())];
|
||||||
|
}
|
||||||
|
} // anonymous
|
||||||
|
|
||||||
|
connection_map::connection_map(std::vector<boost::uuids::uuid> out_connections, const std::size_t stems)
|
||||||
|
: out_mapping_(std::move(out_connections)),
|
||||||
|
in_mapping_(),
|
||||||
|
usage_count_()
|
||||||
|
{
|
||||||
|
// max value is used by `select_stem` as error case
|
||||||
|
if (stems == std::numeric_limits<std::size_t>::max())
|
||||||
|
MONERO_THROW(common_error::kInvalidArgument, "stems value cannot be max size_t");
|
||||||
|
|
||||||
|
usage_count_.resize(stems);
|
||||||
|
if (stems < out_mapping_.size())
|
||||||
|
{
|
||||||
|
for (unsigned i = 0; i < stems; ++i)
|
||||||
|
std::swap(out_mapping_[i], out_mapping_.at(i + crypto::rand_idx(out_mapping_.size() - i)));
|
||||||
|
|
||||||
|
out_mapping_.resize(stems);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
std::shuffle(out_mapping_.begin(), out_mapping_.end(), crypto::random_device{});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
connection_map::~connection_map() noexcept
|
||||||
|
{}
|
||||||
|
|
||||||
|
connection_map connection_map::clone() const
|
||||||
|
{
|
||||||
|
return {*this};
|
||||||
|
}
|
||||||
|
|
||||||
|
bool connection_map::update(std::vector<boost::uuids::uuid> current)
|
||||||
|
{
|
||||||
|
std::sort(current.begin(), current.end());
|
||||||
|
|
||||||
|
bool replace = false;
|
||||||
|
for (auto& existing_out : out_mapping_)
|
||||||
|
{
|
||||||
|
const auto elem = std::lower_bound(current.begin(), current.end(), existing_out);
|
||||||
|
if (elem == current.end() || *elem != existing_out)
|
||||||
|
{
|
||||||
|
existing_out = boost::uuids::nil_uuid();
|
||||||
|
replace = true;
|
||||||
|
}
|
||||||
|
else // already using connection, remove it from candidate list
|
||||||
|
current.erase(elem);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!replace && out_mapping_.size() == usage_count_.size())
|
||||||
|
return false;
|
||||||
|
|
||||||
|
const std::size_t existing_outs = out_mapping_.size();
|
||||||
|
for (std::size_t i = 0; i < usage_count_.size() && !current.empty(); ++i)
|
||||||
|
{
|
||||||
|
const bool increase_stems = out_mapping_.size() <= i;
|
||||||
|
if (increase_stems || out_mapping_[i].is_nil())
|
||||||
|
{
|
||||||
|
std::swap(current.back(), current.at(crypto::rand_idx(current.size())));
|
||||||
|
if (increase_stems)
|
||||||
|
out_mapping_.push_back(current.back());
|
||||||
|
else
|
||||||
|
out_mapping_[i] = current.back();
|
||||||
|
current.pop_back();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return replace || existing_outs < out_mapping_.size();
|
||||||
|
}
|
||||||
|
|
||||||
|
std::size_t connection_map::size() const noexcept
|
||||||
|
{
|
||||||
|
std::size_t count = 0;
|
||||||
|
for (const boost::uuids::uuid& connection : out_mapping_)
|
||||||
|
{
|
||||||
|
if (!connection.is_nil())
|
||||||
|
++count;
|
||||||
|
}
|
||||||
|
return count;
|
||||||
|
}
|
||||||
|
|
||||||
|
boost::uuids::uuid connection_map::get_stem(const boost::uuids::uuid& source)
|
||||||
|
{
|
||||||
|
auto elem = std::lower_bound(in_mapping_.begin(), in_mapping_.end(), source, key_less{});
|
||||||
|
if (elem == in_mapping_.end() || elem->first != source)
|
||||||
|
{
|
||||||
|
const std::size_t index = select_stem(epee::to_span(usage_count_), epee::to_span(out_mapping_));
|
||||||
|
if (out_mapping_.size() < index)
|
||||||
|
return boost::uuids::nil_uuid();
|
||||||
|
|
||||||
|
elem = in_mapping_.emplace(elem, source, index);
|
||||||
|
usage_count_[index]++;
|
||||||
|
}
|
||||||
|
else if (out_mapping_.at(elem->second).is_nil()) // stem connection disconnected after mapping
|
||||||
|
{
|
||||||
|
usage_count_.at(elem->second)--;
|
||||||
|
const std::size_t index = select_stem(epee::to_span(usage_count_), epee::to_span(out_mapping_));
|
||||||
|
if (out_mapping_.size() < index)
|
||||||
|
{
|
||||||
|
in_mapping_.erase(elem);
|
||||||
|
return boost::uuids::nil_uuid();
|
||||||
|
}
|
||||||
|
|
||||||
|
elem->second = index;
|
||||||
|
usage_count_[index]++;
|
||||||
|
}
|
||||||
|
|
||||||
|
return out_mapping_[elem->second];
|
||||||
|
}
|
||||||
|
} // dandelionpp
|
||||||
|
} // net
|
@ -0,0 +1,106 @@
|
|||||||
|
// Copyright (c) 2019, 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 <boost/uuid/uuid.hpp>
|
||||||
|
#include <cstddef>
|
||||||
|
#include <memory>
|
||||||
|
#include <utility>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
#include "span.h"
|
||||||
|
|
||||||
|
namespace net
|
||||||
|
{
|
||||||
|
namespace dandelionpp
|
||||||
|
{
|
||||||
|
//! Assists with mapping source -> stem and tracking connections for stem.
|
||||||
|
class connection_map
|
||||||
|
{
|
||||||
|
// Make sure to update clone method if changing members
|
||||||
|
std::vector<boost::uuids::uuid> out_mapping_; //<! Current outgoing uuid connection at index.
|
||||||
|
std::vector<std::pair<boost::uuids::uuid, std::size_t>> in_mapping_; //<! uuid source to an `out_mapping_` index.
|
||||||
|
std::vector<std::size_t> usage_count_;
|
||||||
|
|
||||||
|
// Use clone method to prevent "hidden" copies.
|
||||||
|
connection_map(const connection_map&) = default;
|
||||||
|
|
||||||
|
public:
|
||||||
|
using value_type = boost::uuids::uuid;
|
||||||
|
using size_type = std::vector<boost::uuids::uuid>::size_type;
|
||||||
|
using difference_type = std::vector<boost::uuids::uuid>::difference_type;
|
||||||
|
using reference = const boost::uuids::uuid&;
|
||||||
|
using const_reference = reference;
|
||||||
|
using iterator = std::vector<boost::uuids::uuid>::const_iterator;
|
||||||
|
using const_iterator = iterator;
|
||||||
|
|
||||||
|
//! Initialized with zero stem connections.
|
||||||
|
explicit connection_map()
|
||||||
|
: connection_map(std::vector<boost::uuids::uuid>{}, 0)
|
||||||
|
{}
|
||||||
|
|
||||||
|
//! Initialized with `out_connections` and `stem_count`.
|
||||||
|
explicit connection_map(std::vector<boost::uuids::uuid> out_connections, std::size_t stems);
|
||||||
|
|
||||||
|
connection_map(connection_map&&) = default;
|
||||||
|
~connection_map() noexcept;
|
||||||
|
connection_map& operator=(connection_map&&) = default;
|
||||||
|
connection_map& operator=(const connection_map&) = delete;
|
||||||
|
|
||||||
|
//! \return An exact duplicate of `this` map.
|
||||||
|
connection_map clone() const;
|
||||||
|
|
||||||
|
//! \return First stem connection.
|
||||||
|
const_iterator begin() const noexcept
|
||||||
|
{
|
||||||
|
return out_mapping_.begin();
|
||||||
|
}
|
||||||
|
|
||||||
|
//! \return One-past the last stem connection.
|
||||||
|
const_iterator end() const noexcept
|
||||||
|
{
|
||||||
|
return out_mapping_.end();
|
||||||
|
}
|
||||||
|
|
||||||
|
/*! Merges in current connections with the previous set of connections.
|
||||||
|
If a connection died, a new one will take its place in the stem or
|
||||||
|
the stem is marked as dead.
|
||||||
|
|
||||||
|
\param connections Current outbound connection ids.
|
||||||
|
\return True if any updates to `get_connections()` was made. */
|
||||||
|
bool update(std::vector<boost::uuids::uuid> current);
|
||||||
|
|
||||||
|
//! \return Number of outgoing connections in use.
|
||||||
|
std::size_t size() const noexcept;
|
||||||
|
|
||||||
|
//! \return Current stem mapping for `source` or `nil_uuid()` if none is possible.
|
||||||
|
boost::uuids::uuid get_stem(const boost::uuids::uuid& source);
|
||||||
|
};
|
||||||
|
} // dandelionpp
|
||||||
|
} // net
|
@ -0,0 +1,586 @@
|
|||||||
|
// Copyright (c) 2019, 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.
|
||||||
|
|
||||||
|
#include <algorithm>
|
||||||
|
#include <boost/uuid/nil_generator.hpp>
|
||||||
|
#include <boost/uuid/random_generator.hpp>
|
||||||
|
#include <boost/uuid/uuid.hpp>
|
||||||
|
#include <cstring>
|
||||||
|
#include <gtest/gtest.h>
|
||||||
|
#include <limits>
|
||||||
|
#include <set>
|
||||||
|
|
||||||
|
#include "byte_slice.h"
|
||||||
|
#include "crypto/crypto.h"
|
||||||
|
#include "cryptonote_basic/connection_context.h"
|
||||||
|
#include "cryptonote_protocol/cryptonote_protocol_defs.h"
|
||||||
|
#include "cryptonote_protocol/levin_notify.h"
|
||||||
|
#include "int-util.h"
|
||||||
|
#include "p2p/net_node.h"
|
||||||
|
#include "net/dandelionpp.h"
|
||||||
|
#include "net/levin_base.h"
|
||||||
|
#include "span.h"
|
||||||
|
|
||||||
|
namespace
|
||||||
|
{
|
||||||
|
class test_endpoint final : public epee::net_utils::i_service_endpoint
|
||||||
|
{
|
||||||
|
boost::asio::io_service& io_service_;
|
||||||
|
std::size_t ref_count_;
|
||||||
|
|
||||||
|
virtual bool do_send(epee::byte_slice message) override final
|
||||||
|
{
|
||||||
|
send_queue_.push_back(std::move(message));
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
virtual bool close() override final
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
virtual bool send_done() override final
|
||||||
|
{
|
||||||
|
throw std::logic_error{"send_done not implemented"};
|
||||||
|
}
|
||||||
|
|
||||||
|
virtual bool call_run_once_service_io() override final
|
||||||
|
{
|
||||||
|
return io_service_.run_one();
|
||||||
|
}
|
||||||
|
|
||||||
|
virtual bool request_callback() override final
|
||||||
|
{
|
||||||
|
throw std::logic_error{"request_callback not implemented"};
|
||||||
|
}
|
||||||
|
|
||||||
|
virtual boost::asio::io_service& get_io_service() override final
|
||||||
|
{
|
||||||
|
return io_service_;
|
||||||
|
}
|
||||||
|
|
||||||
|
virtual bool add_ref() override final
|
||||||
|
{
|
||||||
|
++ref_count_;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
virtual bool release() override final
|
||||||
|
{
|
||||||
|
--ref_count_;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public:
|
||||||
|
test_endpoint(boost::asio::io_service& io_service)
|
||||||
|
: epee::net_utils::i_service_endpoint(),
|
||||||
|
io_service_(io_service),
|
||||||
|
ref_count_(0),
|
||||||
|
send_queue_()
|
||||||
|
{}
|
||||||
|
|
||||||
|
virtual ~test_endpoint() noexcept(false) override final
|
||||||
|
{
|
||||||
|
EXPECT_EQ(0u, ref_count_);
|
||||||
|
}
|
||||||
|
|
||||||
|
std::deque<epee::byte_slice> send_queue_;
|
||||||
|
};
|
||||||
|
|
||||||
|
class test_connection
|
||||||
|
{
|
||||||
|
test_endpoint endpoint_;
|
||||||
|
cryptonote::levin::detail::p2p_context context_;
|
||||||
|
epee::levin::async_protocol_handler<cryptonote::levin::detail::p2p_context> handler_;
|
||||||
|
|
||||||
|
public:
|
||||||
|
test_connection(boost::asio::io_service& io_service, cryptonote::levin::connections& connections, boost::uuids::random_generator& random_generator)
|
||||||
|
: context_(),
|
||||||
|
endpoint_(io_service),
|
||||||
|
handler_(std::addressof(endpoint_), connections, context_)
|
||||||
|
{
|
||||||
|
const_cast<boost::uuids::uuid&>(context_.m_connection_id) = random_generator();
|
||||||
|
handler_.after_init_connection();
|
||||||
|
}
|
||||||
|
|
||||||
|
//\return Number of messages processed
|
||||||
|
std::size_t process_send_queue()
|
||||||
|
{
|
||||||
|
std::size_t count = 0;
|
||||||
|
for ( ; !endpoint_.send_queue_.empty(); ++count, endpoint_.send_queue_.pop_front())
|
||||||
|
{
|
||||||
|
// invalid messages shoudn't be possible in this test;
|
||||||
|
EXPECT_TRUE(handler_.handle_recv(endpoint_.send_queue_.front().data(), endpoint_.send_queue_.front().size()));
|
||||||
|
}
|
||||||
|
return count;
|
||||||
|
}
|
||||||
|
|
||||||
|
const boost::uuids::uuid& get_id() const noexcept
|
||||||
|
{
|
||||||
|
return context_.m_connection_id;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
struct received_message
|
||||||
|
{
|
||||||
|
boost::uuids::uuid connection;
|
||||||
|
int command;
|
||||||
|
std::string payload;
|
||||||
|
};
|
||||||
|
|
||||||
|
class test_receiver : public epee::levin::levin_commands_handler<cryptonote::levin::detail::p2p_context>
|
||||||
|
{
|
||||||
|
std::deque<received_message> invoked_;
|
||||||
|
std::deque<received_message> notified_;
|
||||||
|
|
||||||
|
template<typename T>
|
||||||
|
static std::pair<boost::uuids::uuid, typename T::request> get_message(std::deque<received_message>& queue)
|
||||||
|
{
|
||||||
|
if (queue.empty())
|
||||||
|
throw std::logic_error{"Queue has no received messges"};
|
||||||
|
|
||||||
|
if (queue.front().command != T::ID)
|
||||||
|
throw std::logic_error{"Unexpected ID at front of message queue"};
|
||||||
|
|
||||||
|
epee::serialization::portable_storage storage{};
|
||||||
|
if(!storage.load_from_binary(epee::strspan<std::uint8_t>(queue.front().payload)))
|
||||||
|
throw std::logic_error{"Unable to parse epee binary format"};
|
||||||
|
|
||||||
|
typename T::request request{};
|
||||||
|
if (!request.load(storage))
|
||||||
|
throw std::logic_error{"Unable to load into expected request"};
|
||||||
|
|
||||||
|
boost::uuids::uuid connection = queue.front().connection;
|
||||||
|
queue.pop_front();
|
||||||
|
return {connection, std::move(request)};
|
||||||
|
}
|
||||||
|
|
||||||
|
virtual int invoke(int command, const epee::span<const uint8_t> in_buff, std::string& buff_out, cryptonote::levin::detail::p2p_context& context) override final
|
||||||
|
{
|
||||||
|
buff_out.clear();
|
||||||
|
invoked_.push_back(
|
||||||
|
{context.m_connection_id, command, std::string{reinterpret_cast<const char*>(in_buff.data()), in_buff.size()}}
|
||||||
|
);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
virtual int notify(int command, const epee::span<const uint8_t> in_buff, cryptonote::levin::detail::p2p_context& context) override final
|
||||||
|
{
|
||||||
|
notified_.push_back(
|
||||||
|
{context.m_connection_id, command, std::string{reinterpret_cast<const char*>(in_buff.data()), in_buff.size()}}
|
||||||
|
);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
virtual void callback(cryptonote::levin::detail::p2p_context& context) override final
|
||||||
|
{}
|
||||||
|
|
||||||
|
virtual void on_connection_new(cryptonote::levin::detail::p2p_context&) override final
|
||||||
|
{}
|
||||||
|
|
||||||
|
virtual void on_connection_close(cryptonote::levin::detail::p2p_context&) override final
|
||||||
|
{}
|
||||||
|
|
||||||
|
public:
|
||||||
|
test_receiver()
|
||||||
|
: epee::levin::levin_commands_handler<cryptonote::levin::detail::p2p_context>(),
|
||||||
|
invoked_(),
|
||||||
|
notified_()
|
||||||
|
{}
|
||||||
|
|
||||||
|
virtual ~test_receiver() noexcept override final{}
|
||||||
|
|
||||||
|
std::size_t invoked_size() const noexcept
|
||||||
|
{
|
||||||
|
return invoked_.size();
|
||||||
|
}
|
||||||
|
|
||||||
|
std::size_t notified_size() const noexcept
|
||||||
|
{
|
||||||
|
return notified_.size();
|
||||||
|
}
|
||||||
|
|
||||||
|
template<typename T>
|
||||||
|
std::pair<boost::uuids::uuid, typename T::request> get_invoked()
|
||||||
|
{
|
||||||
|
return get_message<T>(invoked_);
|
||||||
|
}
|
||||||
|
|
||||||
|
template<typename T>
|
||||||
|
std::pair<boost::uuids::uuid, typename T::request> get_notification()
|
||||||
|
{
|
||||||
|
return get_message<T>(notified_);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
class levin_notify : public ::testing::Test
|
||||||
|
{
|
||||||
|
const std::shared_ptr<cryptonote::levin::connections> connections_;
|
||||||
|
std::set<boost::uuids::uuid> connection_ids_;
|
||||||
|
|
||||||
|
public:
|
||||||
|
levin_notify()
|
||||||
|
: ::testing::Test(),
|
||||||
|
connections_(std::make_shared<cryptonote::levin::connections>()),
|
||||||
|
connection_ids_(),
|
||||||
|
random_generator_(),
|
||||||
|
io_service_(),
|
||||||
|
receiver_(),
|
||||||
|
contexts_()
|
||||||
|
{
|
||||||
|
connections_->set_handler(std::addressof(receiver_), nullptr);
|
||||||
|
}
|
||||||
|
|
||||||
|
virtual void TearDown() override final
|
||||||
|
{
|
||||||
|
EXPECT_EQ(0u, receiver_.invoked_size());
|
||||||
|
EXPECT_EQ(0u, receiver_.notified_size());
|
||||||
|
}
|
||||||
|
|
||||||
|
void add_connection()
|
||||||
|
{
|
||||||
|
contexts_.emplace_back(io_service_, *connections_, random_generator_);
|
||||||
|
EXPECT_TRUE(connection_ids_.emplace(contexts_.back().get_id()).second);
|
||||||
|
EXPECT_EQ(connection_ids_.size(), connections_->get_connections_count());
|
||||||
|
}
|
||||||
|
|
||||||
|
cryptonote::levin::notify make_notifier(const std::size_t noise_size)
|
||||||
|
{
|
||||||
|
epee::byte_slice noise = nullptr;
|
||||||
|
if (noise_size)
|
||||||
|
noise = epee::levin::make_noise_notify(noise_size);
|
||||||
|
return cryptonote::levin::notify{io_service_, connections_, std::move(noise)};
|
||||||
|
}
|
||||||
|
|
||||||
|
boost::uuids::random_generator random_generator_;
|
||||||
|
boost::asio::io_service io_service_;
|
||||||
|
test_receiver receiver_;
|
||||||
|
std::deque<test_connection> contexts_;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(make_header, no_expect_return)
|
||||||
|
{
|
||||||
|
static constexpr const std::size_t max_length = std::numeric_limits<std::size_t>::max();
|
||||||
|
|
||||||
|
const epee::levin::bucket_head2 header1 = epee::levin::make_header(1024, max_length, 5601, false);
|
||||||
|
EXPECT_EQ(SWAP64LE(LEVIN_SIGNATURE), header1.m_signature);
|
||||||
|
EXPECT_FALSE(header1.m_have_to_return_data);
|
||||||
|
EXPECT_EQ(SWAP64LE(max_length), header1.m_cb);
|
||||||
|
EXPECT_EQ(SWAP32LE(1024), header1.m_command);
|
||||||
|
EXPECT_EQ(SWAP32LE(LEVIN_PROTOCOL_VER_1), header1.m_protocol_version);
|
||||||
|
EXPECT_EQ(SWAP32LE(5601), header1.m_flags);
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(make_header, expect_return)
|
||||||
|
{
|
||||||
|
const epee::levin::bucket_head2 header1 = epee::levin::make_header(65535, 0, 0, true);
|
||||||
|
EXPECT_EQ(SWAP64LE(LEVIN_SIGNATURE), header1.m_signature);
|
||||||
|
EXPECT_TRUE(header1.m_have_to_return_data);
|
||||||
|
EXPECT_EQ(0u, header1.m_cb);
|
||||||
|
EXPECT_EQ(SWAP32LE(65535), header1.m_command);
|
||||||
|
EXPECT_EQ(SWAP32LE(LEVIN_PROTOCOL_VER_1), header1.m_protocol_version);
|
||||||
|
EXPECT_EQ(0u, header1.m_flags);
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(make_notify, empty_payload)
|
||||||
|
{
|
||||||
|
const epee::byte_slice message = epee::levin::make_notify(443, nullptr);
|
||||||
|
const epee::levin::bucket_head2 header =
|
||||||
|
epee::levin::make_header(443, 0, LEVIN_PACKET_REQUEST, false);
|
||||||
|
ASSERT_EQ(sizeof(header), message.size());
|
||||||
|
EXPECT_TRUE(std::memcmp(std::addressof(header), message.data(), sizeof(header)) == 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(make_notify, with_payload)
|
||||||
|
{
|
||||||
|
std::string bytes(100, 'a');
|
||||||
|
std::generate(bytes.begin(), bytes.end(), crypto::random_device{});
|
||||||
|
|
||||||
|
const epee::byte_slice message = epee::levin::make_notify(443, epee::strspan<std::uint8_t>(bytes));
|
||||||
|
const epee::levin::bucket_head2 header =
|
||||||
|
epee::levin::make_header(443, bytes.size(), LEVIN_PACKET_REQUEST, false);
|
||||||
|
|
||||||
|
ASSERT_EQ(sizeof(header) + bytes.size(), message.size());
|
||||||
|
EXPECT_TRUE(std::memcmp(std::addressof(header), message.data(), sizeof(header)) == 0);
|
||||||
|
EXPECT_TRUE(std::memcmp(bytes.data(), message.data() + sizeof(header), bytes.size()) == 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(make_noise, invalid)
|
||||||
|
{
|
||||||
|
EXPECT_TRUE(epee::levin::make_noise_notify(sizeof(epee::levin::bucket_head2) - 1).empty());
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(make_noise, valid)
|
||||||
|
{
|
||||||
|
static constexpr const std::uint32_t flags =
|
||||||
|
LEVIN_PACKET_BEGIN | LEVIN_PACKET_END;
|
||||||
|
|
||||||
|
const epee::byte_slice noise = epee::levin::make_noise_notify(1024);
|
||||||
|
const epee::levin::bucket_head2 header =
|
||||||
|
epee::levin::make_header(0, 1024 - sizeof(epee::levin::bucket_head2), flags, false);
|
||||||
|
|
||||||
|
ASSERT_EQ(1024, noise.size());
|
||||||
|
EXPECT_TRUE(std::memcmp(std::addressof(header), noise.data(), sizeof(header)) == 0);
|
||||||
|
EXPECT_EQ(1024 - sizeof(header), std::count(noise.cbegin() + sizeof(header), noise.cend(), 0));
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(make_fragment, invalid)
|
||||||
|
{
|
||||||
|
EXPECT_TRUE(epee::levin::make_fragmented_notify(nullptr, 0, nullptr).empty());
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(make_fragment, single)
|
||||||
|
{
|
||||||
|
const epee::byte_slice noise = epee::levin::make_noise_notify(1024);
|
||||||
|
const epee::byte_slice fragment = epee::levin::make_fragmented_notify(noise, 11, nullptr);
|
||||||
|
const epee::levin::bucket_head2 header =
|
||||||
|
epee::levin::make_header(11, 1024 - sizeof(epee::levin::bucket_head2), LEVIN_PACKET_REQUEST, false);
|
||||||
|
|
||||||
|
EXPECT_EQ(1024, noise.size());
|
||||||
|
ASSERT_EQ(1024, fragment.size());
|
||||||
|
EXPECT_TRUE(std::memcmp(std::addressof(header), fragment.data(), sizeof(header)) == 0);
|
||||||
|
EXPECT_EQ(1024 - sizeof(header), std::count(noise.cbegin() + sizeof(header), noise.cend(), 0));
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(make_fragment, multiple)
|
||||||
|
{
|
||||||
|
std::string bytes(1024 * 3 - 150, 'a');
|
||||||
|
std::generate(bytes.begin(), bytes.end(), crypto::random_device{});
|
||||||
|
|
||||||
|
const epee::byte_slice noise = epee::levin::make_noise_notify(1024);
|
||||||
|
epee::byte_slice fragment = epee::levin::make_fragmented_notify(noise, 114, epee::strspan<std::uint8_t>(bytes));
|
||||||
|
|
||||||
|
epee::levin::bucket_head2 header =
|
||||||
|
epee::levin::make_header(0, 1024 - sizeof(epee::levin::bucket_head2), LEVIN_PACKET_BEGIN, false);
|
||||||
|
|
||||||
|
ASSERT_LE(sizeof(header), fragment.size());
|
||||||
|
EXPECT_TRUE(std::memcmp(std::addressof(header), fragment.data(), sizeof(header)) == 0);
|
||||||
|
|
||||||
|
fragment.take_slice(sizeof(header));
|
||||||
|
header.m_flags = LEVIN_PACKET_REQUEST;
|
||||||
|
header.m_cb = bytes.size();
|
||||||
|
header.m_command = 114;
|
||||||
|
|
||||||
|
ASSERT_LE(sizeof(header), fragment.size());
|
||||||
|
EXPECT_TRUE(std::memcmp(std::addressof(header), fragment.data(), sizeof(header)) == 0);
|
||||||
|
|
||||||
|
fragment.take_slice(sizeof(header));
|
||||||
|
|
||||||
|
ASSERT_LE(bytes.size(), fragment.size());
|
||||||
|
EXPECT_TRUE(std::memcmp(bytes.data(), fragment.data(), 1024 - sizeof(header) * 2) == 0);
|
||||||
|
|
||||||
|
bytes.erase(0, 1024 - sizeof(header) * 2);
|
||||||
|
fragment.take_slice(1024 - sizeof(header) * 2);
|
||||||
|
header.m_flags = 0;
|
||||||
|
header.m_cb = 1024 - sizeof(header);
|
||||||
|
header.m_command = 0;
|
||||||
|
|
||||||
|
ASSERT_LE(sizeof(header), fragment.size());
|
||||||
|
EXPECT_TRUE(std::memcmp(std::addressof(header), fragment.data(), sizeof(header)) == 0);
|
||||||
|
|
||||||
|
fragment.take_slice(sizeof(header));
|
||||||
|
|
||||||
|
ASSERT_LE(bytes.size(), fragment.size());
|
||||||
|
EXPECT_TRUE(std::memcmp(bytes.data(), fragment.data(), 1024 - sizeof(header)) == 0);
|
||||||
|
|
||||||
|
bytes.erase(0, 1024 - sizeof(header));
|
||||||
|
fragment.take_slice(1024 - sizeof(header));
|
||||||
|
header.m_flags = LEVIN_PACKET_END;
|
||||||
|
|
||||||
|
ASSERT_LE(sizeof(header), fragment.size());
|
||||||
|
EXPECT_TRUE(std::memcmp(std::addressof(header), fragment.data(), sizeof(header)) == 0);
|
||||||
|
|
||||||
|
fragment.take_slice(sizeof(header));
|
||||||
|
EXPECT_TRUE(std::memcmp(bytes.data(), fragment.data(), bytes.size()) == 0);
|
||||||
|
|
||||||
|
fragment.take_slice(bytes.size());
|
||||||
|
|
||||||
|
EXPECT_EQ(18, std::count(fragment.cbegin(), fragment.cend(), 0));
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_F(levin_notify, defaulted)
|
||||||
|
{
|
||||||
|
cryptonote::levin::notify notifier{};
|
||||||
|
{
|
||||||
|
const auto status = notifier.get_status();
|
||||||
|
EXPECT_FALSE(status.has_noise);
|
||||||
|
EXPECT_FALSE(status.connections_filled);
|
||||||
|
}
|
||||||
|
EXPECT_FALSE(notifier.send_txs({}, random_generator_(), false));
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_F(levin_notify, flood)
|
||||||
|
{
|
||||||
|
cryptonote::levin::notify notifier = make_notifier(0);
|
||||||
|
|
||||||
|
for (unsigned count = 0; count < 10; ++count)
|
||||||
|
add_connection();
|
||||||
|
|
||||||
|
{
|
||||||
|
const auto status = notifier.get_status();
|
||||||
|
EXPECT_FALSE(status.has_noise);
|
||||||
|
EXPECT_FALSE(status.connections_filled);
|
||||||
|
}
|
||||||
|
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<cryptonote::blobdata> 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(), false));
|
||||||
|
|
||||||
|
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(9u, receiver_.notified_size());
|
||||||
|
for (unsigned count = 0; count < 9; ++count)
|
||||||
|
{
|
||||||
|
auto notification = receiver_.get_notification<cryptonote::NOTIFY_NEW_TRANSACTIONS>().second;
|
||||||
|
EXPECT_EQ(txs, notification.txs);
|
||||||
|
EXPECT_TRUE(notification._.empty());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ASSERT_EQ(10u, contexts_.size());
|
||||||
|
{
|
||||||
|
auto context = contexts_.begin();
|
||||||
|
EXPECT_TRUE(notifier.send_txs(txs, context->get_id(), true));
|
||||||
|
|
||||||
|
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(9u, receiver_.notified_size());
|
||||||
|
for (unsigned count = 0; count < 9; ++count)
|
||||||
|
{
|
||||||
|
auto notification = receiver_.get_notification<cryptonote::NOTIFY_NEW_TRANSACTIONS>().second;
|
||||||
|
EXPECT_EQ(txs, notification.txs);
|
||||||
|
EXPECT_FALSE(notification._.empty());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_F(levin_notify, noise)
|
||||||
|
{
|
||||||
|
for (unsigned count = 0; count < 10; ++count)
|
||||||
|
add_connection();
|
||||||
|
|
||||||
|
std::vector<cryptonote::blobdata> txs(1);
|
||||||
|
txs[0].resize(1900, 'h');
|
||||||
|
|
||||||
|
const boost::uuids::uuid incoming_id = random_generator_();
|
||||||
|
cryptonote::levin::notify notifier = make_notifier(2048);
|
||||||
|
|
||||||
|
{
|
||||||
|
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, false));
|
||||||
|
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<cryptonote::NOTIFY_NEW_TRANSACTIONS>().second;
|
||||||
|
EXPECT_EQ(txs, notification.txs);
|
||||||
|
EXPECT_TRUE(notification._.empty());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
txs[0].resize(3000, 'r');
|
||||||
|
EXPECT_TRUE(notifier.send_txs(txs, incoming_id, true));
|
||||||
|
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());
|
||||||
|
}
|
||||||
|
|
||||||
|
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<cryptonote::NOTIFY_NEW_TRANSACTIONS>().second;
|
||||||
|
EXPECT_EQ(txs, notification.txs);
|
||||||
|
EXPECT_TRUE(notification._.empty());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in new issue