mirror of https://github.com/oxen-io/oxen-core.git
593 lines
29 KiB
C++
593 lines
29 KiB
C++
// Copyright (c) 2019, The Loki 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
|
|
|
|
// Please no epee.
|
|
#include "zmq.hpp"
|
|
#include "bt_serialize.h"
|
|
#include <string>
|
|
#include <list>
|
|
#include <unordered_map>
|
|
#include <memory>
|
|
#include <functional>
|
|
#include <thread>
|
|
#include <mutex>
|
|
#include <iostream>
|
|
#include <chrono>
|
|
|
|
namespace quorumnet {
|
|
|
|
/// Logging levels passed into LogFunc
|
|
enum class LogLevel { trace, debug, info, warn, error, fatal };
|
|
|
|
using namespace std::chrono_literals;
|
|
|
|
/**
|
|
* Class that represents a listening service node on the quorum p2p network. This object supports
|
|
* connecting to service node peers and handles requests from node clients (for example, a remote
|
|
* node submitting a Blink transaction to a Blink quorum).
|
|
*
|
|
* Internally the class uses a worker thread to handle messages from other service nodes, which it
|
|
* then passed back into calling code via callbacks.
|
|
*/
|
|
class SNNetwork {
|
|
public:
|
|
/// The function type for looking up where to connect to the SN with the given pubkey. Should
|
|
/// return an empty string for an invalid or unknown pubkey or one without a known address.
|
|
using LookupFunc = std::function<std::string(const std::string &pubkey)>;
|
|
|
|
enum class allow { denied, client, service_node };
|
|
/// Callback type invoked to determine whether the given ip and pubkey are allowed to connect to
|
|
/// us as a SN, client, or not at all.
|
|
///
|
|
/// @param ip - the ip address of the incoming connection; will be empty if called to attempt to
|
|
/// "upgrade" the permission of an existing non-SN connection.
|
|
/// @param pubkey - the curve25519 pubkey (which is calculated from the SN ed25519 pubkey) of
|
|
/// the connecting service node (32 byte string), or an empty string if this is a client
|
|
/// connection without remote SN authentication.
|
|
/// @returns an `allow` enum value: `denied` if the connection is not allowed, `client` if the
|
|
/// connection is allowed as a client (i.e. not for SN-to-SN commands), `service_node` if the
|
|
/// connection is a valid SN.
|
|
using AllowFunc = std::function<allow(const std::string &ip, const std::string &pubkey)>;
|
|
|
|
/// Function pointer to ask whether a log of the given level is wanted. If it returns true the
|
|
/// log message will be built and then passed to Log.
|
|
using WantLog = bool (*)(LogLevel);
|
|
///
|
|
/// call to get somewhere to log to when there is a logging message. If it
|
|
/// returns a std::ostream pointer then output is sent to it; otherwise (i.e. nullptr) output is
|
|
/// suppressed. Takes three arguments: the log level, the __FILE__ value, and the __LINE__ value.
|
|
using WriteLog = void (*)(LogLevel level, const char *file, int line, std::string msg);
|
|
|
|
/// Explicitly non-copyable, non-movable because most things here aren't copyable, and a few
|
|
/// things aren't movable. If you need to pass the SNNetwork around, wrap it in a unique_ptr.
|
|
SNNetwork(const SNNetwork &) = delete;
|
|
SNNetwork &operator=(const SNNetwork &) = delete;
|
|
SNNetwork(SNNetwork &&) = delete;
|
|
SNNetwork &operator=(SNNetwork &&) = delete;
|
|
|
|
/// Encapsulates an incoming message from a remote node with message details plus extra info
|
|
/// need to send a reply back through the proxy thread via the `reply()` method.
|
|
class message {
|
|
private:
|
|
SNNetwork &net;
|
|
public:
|
|
std::string command; ///< The command name
|
|
std::vector<bt_value> data; ///< The provided command data parts, if any.
|
|
const std::string pubkey; ///< The originator pubkey (32 bytes)
|
|
const bool sn; ///< True if the pubkey is from a SN, meaning we can/should reconnect to it if necessary
|
|
|
|
/// Constructor
|
|
message(SNNetwork &net, std::string command, std::string pubkey, bool sn)
|
|
: net{net}, command{std::move(command)}, pubkey{std::move(pubkey)}, sn{sn} {
|
|
assert(this->pubkey.size() == 32);
|
|
}
|
|
|
|
/// Sends a reply. Arguments are forward to send() but with send_option::optional{} added
|
|
/// if the originator is not a SN. For SN messages (i.e. where `sn` is true) this is a
|
|
/// "strong" reply by default in that the proxy will establish a new connection to the SN if
|
|
/// no longer connected. For non-SN messages the reply will be attempted using the
|
|
/// available routing information, but if the connection has already been closed the reply
|
|
/// will be dropped.
|
|
template <typename... Args>
|
|
void reply(const std::string &command, Args &&...args);
|
|
};
|
|
|
|
/// Opaque pointer sent to the callbacks, to allow for including arbitrary state data (for
|
|
/// example, an owning object or state data). Defaults to nullptr and has to be explicitly set
|
|
/// if desired.
|
|
void *data = nullptr;
|
|
|
|
/// Possible command types; see register_command
|
|
enum class command_type { quorum, public_, response };
|
|
|
|
private:
|
|
zmq::context_t context;
|
|
|
|
/// A unique id for this SNNetwork, assigned in a thread-safe manner during construction.
|
|
const int object_id;
|
|
|
|
/// The keypair of this SN, either provided or generated during construction
|
|
std::string pubkey, privkey;
|
|
|
|
/// The thread in which most of the intermediate work happens (handling external connections
|
|
/// and proxying requests between them to worker threads)
|
|
std::thread proxy_thread;
|
|
|
|
/// Called to obtain a "command" socket that attaches to `control` to send commands to the
|
|
/// proxy thread from other threads. This socket is unique per thread and SNNetwork instance.
|
|
zmq::socket_t &get_control_socket();
|
|
/// Stores all of the sockets created in different threads via `get_control_socket`. This is
|
|
/// only used during destruction to close all of those open sockets, and is protected by an
|
|
/// internal mutex which is only locked by new threads getting a control socket and the
|
|
/// destructor.
|
|
std::vector<std::shared_ptr<zmq::socket_t>> thread_control_sockets;
|
|
|
|
|
|
|
|
|
|
///////////////////////////////////////////////////////////////////////////////////
|
|
/// NB: The following are all the domain of the proxy thread (once it is started)!
|
|
|
|
/// The lookup function that tells us where to connect to a peer
|
|
LookupFunc peer_lookup;
|
|
/// Our listening socket for public connections, or null for a remote-only
|
|
/// (TODO: change this to std::optional once we use C++17).
|
|
std::unique_ptr<zmq::socket_t> listener;
|
|
|
|
/// Callback to see whether the incoming connection is allowed
|
|
AllowFunc allow_connection;
|
|
|
|
/// Callback to see if we want a log message at the given level.
|
|
WantLog want_logs;
|
|
/// If want_logs returns true, the log message is build and passed into this.
|
|
WriteLog logger;
|
|
|
|
/// Info about a peer's established connection to us. Note that "established" means both
|
|
/// connected and authenticated.
|
|
struct peer_info {
|
|
/// True if we've authenticated this peer as a service node. (Note that new outgoing
|
|
/// connections are *always* expected to go to service nodes and will update this to true).
|
|
bool service_node = false;
|
|
|
|
/// Will be set to a non-empty routing prefix (which needs to be prefixed out outgoing
|
|
/// messages) if we have (or at least recently had) an established incoming connection with
|
|
/// this peer. Will be empty if there is no incoming connection.
|
|
std::string incoming;
|
|
|
|
/// The index in `remotes` if we have an established outgoing connection to this peer, -1 if
|
|
/// we have no outgoing connection to this peer.
|
|
int outgoing = -1;
|
|
|
|
/// The last time we sent or received a message (or had some other relevant activity) with
|
|
/// this peer. Used for closing outgoing connections that have reached an inactivity expiry
|
|
/// time.
|
|
std::chrono::steady_clock::time_point last_activity;
|
|
|
|
/// Updates last_activity to the current time
|
|
void activity() { last_activity = std::chrono::steady_clock::now(); }
|
|
|
|
/// After more than this much activity we will close an idle connection
|
|
std::chrono::milliseconds idle_expiry;
|
|
};
|
|
|
|
struct pk_hash { size_t operator()(const std::string &pubkey) const { size_t h; std::memcpy(&h, pubkey.data(), sizeof(h)); return h; } };
|
|
|
|
/// Currently peer connections, pubkey -> peer_info
|
|
std::unordered_map<std::string, peer_info, pk_hash> peers;
|
|
|
|
/// different polling sockets the proxy handler polls: this always contains some internal
|
|
/// sockets for inter-thread communication followed by listener socket and a pollitem for every
|
|
/// (outgoing) remote socket in `remotes`. This must be in a sequential vector because of zmq
|
|
/// requirements (otherwise it would be far nicer to not have to synchronize the two vectors).
|
|
std::vector<zmq::pollitem_t> pollitems;
|
|
|
|
/// Properly adds a socket to poll for input to pollitems
|
|
void add_pollitem(zmq::socket_t &sock);
|
|
|
|
/// The number of internal sockets in `pollitems`
|
|
static constexpr size_t poll_internal_size = 3;
|
|
|
|
/// The pollitems location corresponding to `remotes[0]`.
|
|
const size_t poll_remote_offset; // will be poll_internal_size + 1 for a full listener (the +1 is the listening socket); poll_internal_size for a remote-only
|
|
|
|
/// The outgoing remote connections we currently have open along with the remote pubkeys. Note
|
|
/// that the sockets here are generally accessed via the weak_ptr inside the `peers` element.
|
|
/// Each element [i] here corresponds to an the pollitem_t at pollitems[i+1+poll_internal_size].
|
|
/// (Ideally we'd use one structure, but zmq requires the pollitems be in contiguous storage).
|
|
std::vector<std::pair<std::string, zmq::socket_t>> remotes;
|
|
|
|
/// Socket we listen on to receive control messages in the proxy thread. Each thread has its own
|
|
/// internal "control" connection (returned by `get_control_socket()`) to this socket used to
|
|
/// give instructions to the proxy such as instructing it to initiate a connection to a remote
|
|
/// or send a message.
|
|
zmq::socket_t command{context, zmq::socket_type::router};
|
|
|
|
/// Router socket to reach internal worker threads from proxy
|
|
zmq::socket_t workers{context, zmq::socket_type::router};
|
|
|
|
/// Starts a new worker thread with the given id. Note that the worker may not yet be ready
|
|
/// until a READY signal arrives on the worker socket.
|
|
void spawn_worker(std::string worker_id);
|
|
|
|
/// Worker threads (ZMQ id -> thread)
|
|
std::unordered_map<std::string, std::thread> worker_threads;
|
|
|
|
/// ZMQ ids of idle, active workers
|
|
std::list<std::string> idle_workers;
|
|
|
|
/// Maximum number of worker threads created on demand up to this limit.
|
|
unsigned int max_workers;
|
|
|
|
/// Worker thread loop
|
|
void worker_thread(std::string worker_id);
|
|
|
|
/// Does the proxying work
|
|
void proxy_loop(const std::vector<std::string> &bind);
|
|
|
|
/// Forwards an incoming message to an idle worker, removing the idle worker from the queue
|
|
void proxy_to_worker(size_t conn_index, std::list<zmq::message_t> &&parts);
|
|
|
|
/// proxy thread command handlers for commands sent from the outer object QUIT. This doesn't
|
|
/// get called immediately on a QUIT command: the QUIT commands tells workers to quit, then this
|
|
/// gets called after all works have done so.
|
|
void proxy_quit();
|
|
|
|
/// Common connection implementation used by proxy_connect/proxy_send. Returns the socket
|
|
/// and, if a routing prefix is needed, the required prefix (or an empty string if not needed).
|
|
/// For an optional connect that fail, returns nullptr for the socket.
|
|
std::pair<zmq::socket_t *, std::string> proxy_connect(const std::string &pubkey, const std::string &connect_hint, bool optional, bool incoming_only, std::chrono::milliseconds keep_alive);
|
|
|
|
/// CONNECT command telling us to connect to a new pubkey. Returns the socket (which could be
|
|
/// existing or a new one).
|
|
std::pair<zmq::socket_t *, std::string> proxy_connect(bt_dict &&data);
|
|
|
|
/// DISCONNECT command telling us to disconnect out remote connection to the given pubkey (if we
|
|
/// have one).
|
|
void proxy_disconnect(const std::string &pubkey);
|
|
|
|
/// SEND command. Does a connect first, if necessary.
|
|
void proxy_send(bt_dict &&data);
|
|
|
|
/// REPLY command. Like SEND, but only has a listening socket route to send back to and so is
|
|
/// weaker (i.e. it cannot reconnect to the SN if the connection is no longer open).
|
|
void proxy_reply(bt_dict &&data);
|
|
|
|
/// ZAP (https://rfc.zeromq.org/spec:27/ZAP/) authentication handler; this is called with the
|
|
/// zap auth socket to do non-blocking processing of any waiting authentication requests waiting
|
|
/// on it to verify whether the connection is from a valid/allowed SN.
|
|
void process_zap_requests(zmq::socket_t &zap_auth);
|
|
|
|
/// Handles a control message from some outer thread to the proxy
|
|
void proxy_control_message(std::list<zmq::message_t> parts);
|
|
|
|
/// Closing any idle connections that have outlived their idle time. Note that this only
|
|
/// affects outgoing connections; incomings connections are the responsibility of the other end.
|
|
void proxy_expire_idle_peers();
|
|
|
|
/// Closes an outgoing connection immediately, updates internal variables appropriately.
|
|
/// Returns the next iterator (the original may or may not be removed from peers, depending on
|
|
/// whether or not it also has an active incoming connection).
|
|
decltype(peers)::iterator proxy_close_outgoing(decltype(peers)::iterator it);
|
|
|
|
|
|
/// End of proxy-specific members
|
|
///////////////////////////////////////////////////////////////////////////////////
|
|
|
|
|
|
|
|
|
|
/// Callbacks for data commands. Must be fully populated before starting SNNetwork instances
|
|
/// as this is accessed without a lock from worker threads.
|
|
///
|
|
/// The value is the {callback, public} pair where `public` is true if unauthenticated
|
|
/// connections may call this and false if only authenricated SNs may invoke the command.
|
|
static std::unordered_map<std::string, std::pair<void(*)(SNNetwork::message &message, void *data), command_type>> commands;
|
|
static bool commands_mutable;
|
|
|
|
/// Starts up the proxy thread; called during construction
|
|
void launch_proxy_thread(const std::vector<std::string> &bind);
|
|
|
|
public:
|
|
/**
|
|
* Constructs a SNNetwork connection listening on the given bind string.
|
|
*
|
|
* @param pubkey the service node's public key (32-byte binary string)
|
|
* @param privkey the service node's private key (32-byte binary string)
|
|
* @param bind list of addresses to bind to. Can be any string zmq supports; typically a tcp
|
|
* IP/port combination such as: "tcp://\*:4567" or "tcp://1.2.3.4:5678".
|
|
* @param peer_lookup function that takes a pubkey key (32-byte binary string) and returns a
|
|
* connection string such as "tcp://1.2.3.4:23456" to which a connection should be established
|
|
* to reach that service node. Note that this function is only called if there is no existing
|
|
* connection to that service node, and that the function is never called for a connection to
|
|
* self (that uses an internal connection instead).
|
|
* @param allow_incoming called on incoming connections with the (verified) incoming connection
|
|
* pubkey (32-byte binary string) to determine whether the given SN should be allowed to
|
|
* connect.
|
|
* @param want_log
|
|
* @param log a function pointer (or non-capturing lambda) to call to get a std::ostream pointer
|
|
* to send output to, or nullptr to suppress output. Optional; if omitted the default returns
|
|
* std::cerr for WARN and higher.
|
|
* @param max_workers the maximum number of simultaneous worker threads to start. Defaults to
|
|
* std::thread::hardware_concurrency(). Note that threads are only started on demand (i.e. when
|
|
* a request arrives when all existing threads are busy handling requests).
|
|
*/
|
|
SNNetwork(std::string pubkey,
|
|
std::string privkey,
|
|
const std::vector<std::string> &bind,
|
|
LookupFunc peer_lookup,
|
|
AllowFunc allow_connection,
|
|
WantLog want_log = [](LogLevel l) { return l >= LogLevel::warn; },
|
|
WriteLog logger = [](LogLevel, const char *f, int line, std::string msg) { std::cerr << f << ':' << line << ": " << msg << std::endl; },
|
|
unsigned int max_workers = std::thread::hardware_concurrency());
|
|
|
|
/** Constructs a SNNetwork that does not listen but can be used for connecting to remote
|
|
* listening service nodes, for example to submit blink transactions to service nodes. It
|
|
* generates a non-persistant x25519 key pair on startup (for encrypted communication with
|
|
* peers).
|
|
*/
|
|
SNNetwork(LookupFunc peer_lookup,
|
|
AllowFunc allow_connection,
|
|
WantLog want_log = [](LogLevel l) { return l >= LogLevel::warn; },
|
|
WriteLog logger = [](LogLevel, const char *f, int line, std::string msg) { std::cerr << f << ':' << line << ": " << msg << std::endl; },
|
|
unsigned int max_workers = std::thread::hardware_concurrency());
|
|
|
|
/**
|
|
* Destructor; instructs the proxy to quit. The proxy tells all workers to quit, waits for them
|
|
* to quit and rejoins the threads then quits itself. The outer thread (where the destructor is
|
|
* running) rejoins the proxy thread.
|
|
*/
|
|
~SNNetwork();
|
|
|
|
/**
|
|
* Returns true if we are running as a service node, which (currently) is synonymous with us
|
|
* being started in listening mode.
|
|
*/
|
|
bool is_service_node() const { return (bool) listener; }
|
|
|
|
/**
|
|
* Try to initiate a connection to the given SN in anticipation of needing a connection in the
|
|
* future. If a connection is already established, the connection's idle timer will be reset
|
|
* (so that the connection will not be closed too soon). If the given idle timeout is greater
|
|
* than the current idle timeout then the timeout increases to the new value; if less than the
|
|
* current timeout it is ignored. (Note that idle timeouts only apply if the existing
|
|
* connection is an outgoing connection).
|
|
*
|
|
* Note that this method (along with send) doesn't block waiting for a connection; it merely
|
|
* instructs the proxy thread that it should establish a connection.
|
|
*
|
|
* @param pubkey - the public key (32-byte binary string) of the service node to connect to
|
|
* @param keep_alive - the connection will be kept alive if there was valid activity within
|
|
* the past `keep_alive` milliseconds. If an outgoing connection already
|
|
* exists, the longer of the existing and the given keep alive is used.
|
|
* Note that the default applied here is much longer than the default for an
|
|
* implicit connect() by calling send() directly.
|
|
* @param hint - if non-empty and a new outgoing connection needs to be made this hint value
|
|
* may be used instead of calling the lookup function. (Note that there is no
|
|
* guarantee that the hint will be used; it should only be used when the lookup
|
|
* value has been precomputed to save a lookup call).
|
|
*/
|
|
void connect(const std::string &pubkey, std::chrono::milliseconds keep_alive = 5min, const std::string &hint = "");
|
|
|
|
/**
|
|
* Queue a message to be relayed to SN identified with the given pubkey without expecting a
|
|
* reply. The SN will attempt to relay (first connecting and handshaking if not already
|
|
* connected to the given SN).
|
|
*
|
|
* If a new connection it established it will have a relatively short (15s) idle timeout. If
|
|
* the connection should stay open longer you should call `connect(pubkey, IDLETIME)` first.
|
|
*
|
|
* Note that this method (along with connect) doesn't block waiting for a connection or for the
|
|
* message to send; it merely instructs the proxy thread that it should send.
|
|
*
|
|
* @param pubkey - the pubkey to send this to
|
|
* @param value - a bt_serializable value to serialize and send
|
|
* @param opts - any number of bt_serializable values and send options. Each send option affect
|
|
* how the send works; each serializable value becomes a serialized message part.
|
|
*
|
|
* Example:
|
|
*
|
|
* sn.send(pubkey, "hello", "abc", 42, send_option::hint("tcp://localhost:1234"));
|
|
*
|
|
* sends the command `hello` to the given pubkey, containing two additional message parts of
|
|
* serialized "abc" and 42 values, and, if not currently connected, using the given connection
|
|
* hint rather than performing a connection address lookup on the pubkey.
|
|
*/
|
|
template <typename... T>
|
|
void send(const std::string &pubkey, const std::string &cmd, const T &...opts);
|
|
|
|
/** The keep-alive time for a send() that results in a new connection. To use a longer
|
|
* keep-alive to a host call `connect()` first with the desired keep-alive time or pass the
|
|
* send_option::keep_alive
|
|
*/
|
|
static constexpr std::chrono::milliseconds default_send_keep_alive{15000};
|
|
|
|
/// The key pair this SN was created with
|
|
const std::string &get_pubkey() const { return pubkey; }
|
|
const std::string &get_privkey() const { return privkey; }
|
|
|
|
/**
|
|
* Registers a command that may be invoked on a quorumnet connection. The quorum command
|
|
* is one of three types:
|
|
* - `SNNetwork::command_type::quorum` - for a command that is only permitted between
|
|
* registered service nodes. It will be discarded if received from a remote that is not
|
|
* recognized as a service node, or if the local node is not a service node.
|
|
* - `SNNetwork::command_type::public_` - for a command that is permitted from anyone, but
|
|
* only if the local node is running as a service node.
|
|
* - `SNNetwork::command_type::response` - for a command that can be issued by a service
|
|
* node to the local client (whether or not running as a service node), typically issued
|
|
* in response to a `public_` command issued by this service node.
|
|
*
|
|
* Commands may only be registered before any SNNetwork instance has been constructed.
|
|
*
|
|
* @param command - the command string to assign. If it already exists it will be replaced.
|
|
* @param callback - a callback that takes the message info and the opaque `data` pointer
|
|
* @param cmd_type - the command type, as described above.
|
|
*/
|
|
static void register_command(
|
|
std::string command,
|
|
command_type cmd_type,
|
|
void(*callback)(SNNetwork::message &message, void *data));
|
|
};
|
|
|
|
/// Namespace for options to the send() method
|
|
namespace send_option {
|
|
|
|
/// `serialized` lets you serialize once when sending the same data to many peers by constructing a
|
|
/// single serialized option and passing it repeatedly rather than needing to reserialize on each
|
|
/// send.
|
|
struct serialized {
|
|
std::string data;
|
|
template <typename T>
|
|
serialized(const T &arg) : data{quorumnet::bt_serialize(arg)} {}
|
|
};
|
|
|
|
/// Specifies a connection hint when passed in to send(). If there is no current connection to the
|
|
/// peer then the hint is used to save a call to the LookupFunc to get the connection location.
|
|
/// (Note that there is no guarantee that the given hint will be used or that a LookupFunc call will
|
|
/// not be done.)
|
|
struct hint {
|
|
std::string connect_hint;
|
|
hint(std::string connect_hint) : connect_hint{std::move(connect_hint)} {}
|
|
};
|
|
|
|
/// Does a send() if we already have a connection (incoming or outgoing) with the given peer,
|
|
/// otherwise drops the message.
|
|
struct optional {};
|
|
|
|
/// Specifies that the message should be sent only if it can be sent on an existing incoming socket,
|
|
/// and dropped otherwise.
|
|
struct incoming {};
|
|
|
|
/// Specifies the idle timeout for the connection - if a new or existing outgoing connection is used
|
|
/// for the send and its current idle timeout setting is less than this value then it is updated.
|
|
struct keep_alive {
|
|
std::chrono::milliseconds time;
|
|
keep_alive(std::chrono::milliseconds time) : time{std::move(time)} {}
|
|
};
|
|
|
|
}
|
|
|
|
namespace detail {
|
|
|
|
// Sends a control message to the given socket consisting of the command plus optional dict
|
|
// data (only sent if the dict is non-empty).
|
|
void send_control(zmq::socket_t &sock, const std::string &cmd, const bt_dict &data = {});
|
|
|
|
/// Base case: takes a serializable value and appends it to the message parts
|
|
template <typename T>
|
|
void apply_send_option(bt_list &parts, bt_dict &, const T &arg) {
|
|
parts.push_back(quorumnet::bt_serialize(arg));
|
|
}
|
|
|
|
/// `serialized` specialization: lets you serialize once when sending the same data to many peers
|
|
template <> inline void apply_send_option(bt_list &parts, bt_dict &, const send_option::serialized &serialized) {
|
|
parts.push_back(serialized.data);
|
|
}
|
|
|
|
/// `hint` specialization: sets the hint in the control data
|
|
template <> inline void apply_send_option(bt_list &, bt_dict &control_data, const send_option::hint &hint) {
|
|
control_data["hint"] = hint.connect_hint;
|
|
}
|
|
|
|
/// `optional` specialization: sets the optional flag in the control data
|
|
template <> inline void apply_send_option(bt_list &, bt_dict &control_data, const send_option::optional &) {
|
|
control_data["optional"] = 1;
|
|
}
|
|
|
|
/// `incoming` specialization: sets the optional flag in the control data
|
|
template <> inline void apply_send_option(bt_list &, bt_dict &control_data, const send_option::incoming &) {
|
|
control_data["incoming"] = 1;
|
|
}
|
|
|
|
/// `keep_alive` specialization: increases the outgoing socket idle timeout (if shorter)
|
|
template <> inline void apply_send_option(bt_list &, bt_dict &control_data, const send_option::keep_alive &timeout) {
|
|
control_data["keep-alive"] = timeout.time.count();
|
|
}
|
|
|
|
/// Calls apply_send_option on each argument and returns a bt_dict with the command plus data stored
|
|
/// in the "send" key plus whatever else is implied by any given option arguments.
|
|
template <typename... T>
|
|
bt_dict send_control_data(const std::string &cmd, const T &...opts) {
|
|
bt_dict control_data;
|
|
bt_list parts{{cmd}};
|
|
#ifdef __cpp_fold_expressions
|
|
(detail::apply_send_option(parts, control_data, opts),...);
|
|
#else
|
|
(void) std::initializer_list<int>{(detail::apply_send_option(parts, control_data, opts), 0)...};
|
|
#endif
|
|
|
|
control_data["send"] = std::move(parts);
|
|
return control_data;
|
|
}
|
|
|
|
}
|
|
|
|
template <typename... T>
|
|
void SNNetwork::send(const std::string &pubkey, const std::string &cmd, const T &...opts) {
|
|
bt_dict control_data = detail::send_control_data(cmd, opts...);
|
|
control_data["pubkey"] = pubkey;
|
|
detail::send_control(get_control_socket(), "SEND", control_data);
|
|
}
|
|
|
|
template <typename... Args>
|
|
void SNNetwork::message::reply(const std::string &command, Args &&...args) {
|
|
if (sn) net.send(pubkey, command, std::forward<Args>(args)...);
|
|
else net.send(pubkey, command, send_option::optional{}, std::forward<Args>(args)...);
|
|
}
|
|
|
|
// Creates a hex string from a character sequence.
|
|
template <typename It>
|
|
std::string as_hex(It begin, It end) {
|
|
constexpr std::array<char, 16> lut{{'0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f'}};
|
|
std::string hex;
|
|
using std::distance;
|
|
hex.reserve(distance(begin, end) * 2);
|
|
while (begin != end) {
|
|
char c = *begin;
|
|
hex += lut[(c & 0xf0) >> 4];
|
|
hex += lut[c & 0x0f];
|
|
++begin;
|
|
}
|
|
return hex;
|
|
}
|
|
|
|
template <typename String>
|
|
inline std::string as_hex(const String &s) {
|
|
using std::begin;
|
|
using std::end;
|
|
return as_hex(begin(s), end(s));
|
|
}
|
|
|
|
|
|
|
|
}
|
|
|
|
// vim:sw=4:et
|