oxen-storage-server/httpserver/swarm.cpp
Jason Rhinelander 3ad1ea6c7c Remove boost beast & many intertwined changes
- Replace boost beast http(s) client code with cpr (same as used in
oxen-core and lokinet)

- Replace bootstrap http requests with encrypted/authenticated omq rpc
requests to the bootstrap nodes (rather than unauthenticated,
unencrypted http connections).

- Remove boost::asio

- Remove boost circular buffer; a regular map with a two-line trimming
code is simpler for block hash cache, and a limit on stored snodes
doesn't seem necessary for the rate limiter.

- Make rate_limiter clean itself periodically; currently it only cleans
once it reaches max clients (i.e. 10k).

- Make rate_limiter thread safe, plus various code cleanups/DRYing.

- Remove ip_utils; we now longer really need a check for public ip for
proxy requests because we are sufficiently restrictive on the proxy url
target that we are unlikely to be able to reach anything sensitive (and
we also don't allow redirects).

- Replace /swarms/ping_test_v1 with /ping_test/v1; this new request now
returns the remote pubkey in a header, and no longer includes an SSL
cert signature (so that we can drop the SSL cert signatures after HF19).
The old one will still be used until HF19.

- Add OMQ endpoint for storage tests; it gets used instead of the https
one at HF19.

- Refactor storage test retries into request_handler (needed so both
legacy and omq endpoints can use it).

- Fix HF18 swarms/storage_test endpoint which was missing the snode
signature required for current SS versions to accept the response.

- Move HTTPS server-specific code (validate_snode_signature) out of
RequestHandler into HTTPSServer

- Make onion proxy-to-url timeout a bit less than the onion request
timeout so that the client has a better chance of actually getting a
timeout error.

- Miscellaneous cleanups

- Remove pre-HF18 compatibility code.

- Remove `process_lns_request` onion request target: it was accidentally
broken in HF18; clients are now using an oxend rpc request proxy to
`ons_resolve` instead.

- Shorten timeout values for ping and storage tests (5s and 15s,
respectively) and bootstrap connections (now 10s instead of 60s).
2021-06-16 18:59:06 -03:00

379 lines
11 KiB
C++

#include "swarm.h"
#include "oxen_logger.h"
#include "service_node.h"
#include <cstdlib>
#include <ostream>
#include <unordered_map>
#include "utils.hpp"
namespace oxen {
static bool swarm_exists(const all_swarms_t& all_swarms,
const swarm_id_t& swarm) {
const auto it = std::find_if(
all_swarms.begin(), all_swarms.end(),
[&swarm](const SwarmInfo& si) { return si.swarm_id == swarm; });
return it != all_swarms.end();
}
void debug_print(std::ostream& os, const block_update_t& bu) {
os << "Block update: {\n";
os << " height: " << bu.height << '\n';
os << " block hash: " << bu.block_hash << '\n';
os << " hardfork: " << bu.hardfork << '\n';
os << " swarms: [\n";
for (const SwarmInfo& swarm : bu.swarms) {
os << " {\n";
os << " id: " << swarm.swarm_id << '\n';
os << " }\n";
}
os << " ]\n";
os << "}\n";
}
Swarm::~Swarm() = default;
bool Swarm::is_existing_swarm(swarm_id_t sid) const {
return std::any_of(all_valid_swarms_.begin(), all_valid_swarms_.end(),
[sid](const SwarmInfo& cur_swarm_info) {
return cur_swarm_info.swarm_id == sid;
});
}
SwarmEvents Swarm::derive_swarm_events(const all_swarms_t& swarms) const {
SwarmEvents events = {};
const auto our_swarm_it = std::find_if(
swarms.begin(), swarms.end(), [this](const SwarmInfo& swarm_info) {
const auto& snodes = swarm_info.snodes;
return std::find(snodes.begin(), snodes.end(), our_address_) !=
snodes.end();
});
if (our_swarm_it == swarms.end()) {
// We are not in any swarm, nothing to do
events.our_swarm_id = INVALID_SWARM_ID;
return events;
}
const auto& new_swarm_snodes = our_swarm_it->snodes;
const auto new_swarm_id = our_swarm_it->swarm_id;
events.our_swarm_id = new_swarm_id;
events.our_swarm_members = new_swarm_snodes;
if (cur_swarm_id_ == INVALID_SWARM_ID) {
// Only started in a swarm, nothing to do at this stage
return events;
}
if (cur_swarm_id_ != new_swarm_id) {
// Got moved to a new swarm
if (!swarm_exists(swarms, cur_swarm_id_)) {
// Dissolved, new to push all our data to new swarms
events.dissolved = true;
}
// If our old swarm is still alive, there is nothing for us to do
return events;
}
/// --- WE are still in the same swarm if we reach here ---
/// See if anyone joined our swarm
for (const auto& sn : new_swarm_snodes) {
const auto it = std::find(swarm_peers_.begin(), swarm_peers_.end(), sn);
if (it == swarm_peers_.end() && sn != our_address_) {
events.new_snodes.push_back(sn);
}
}
/// See if there are any new swarms
for (const auto& swarm_info : swarms) {
const bool found = this->is_existing_swarm(swarm_info.swarm_id);
if (!found) {
events.new_swarms.push_back(swarm_info.swarm_id);
}
}
/// NOTE: need to be careful and make sure we don't miss any
/// swarm update (e.g. if we don't update frequently enough)
return events;
}
void Swarm::set_swarm_id(swarm_id_t sid) {
if (sid == INVALID_SWARM_ID) {
OXEN_LOG(warn, "We are not currently an active Service Node");
} else {
if (cur_swarm_id_ == INVALID_SWARM_ID) {
OXEN_LOG(info, "EVENT: started SN in swarm: {}", sid);
} else if (cur_swarm_id_ != sid) {
OXEN_LOG(info, "EVENT: got moved into a new swarm: {}", sid);
}
}
cur_swarm_id_ = sid;
}
static auto get_snode_map_from_swarms(const all_swarms_t& swarms) {
std::unordered_map<legacy_pubkey, sn_record_t> snode_map;
for (const auto& swarm : swarms) {
for (const auto& snode : swarm.snodes) {
snode_map.emplace(snode.pubkey_legacy, snode);
}
}
return snode_map;
}
template <typename T>
bool update_if_changed(T& val, const T& new_val, const std::common_type_t<T>& ignore_val) {
if (new_val != ignore_val && new_val != val) {
val = new_val;
return true;
}
return false;
}
auto apply_ips(const all_swarms_t& swarms_to_keep,
const all_swarms_t& other_swarms) -> all_swarms_t {
all_swarms_t result_swarms = swarms_to_keep;
const auto other_snode_map = get_snode_map_from_swarms(other_swarms);
int updates_count = 0;
for (auto& [swarm_id, snodes] : result_swarms) {
for (auto& snode : snodes) {
const auto other_snode_it =
other_snode_map.find(snode.pubkey_legacy);
if (other_snode_it != other_snode_map.end()) {
auto& sn = other_snode_it->second;
// Keep swarms_to_keep but don't overwrite with default IPs/ports
bool updated = false;
if (update_if_changed(snode.ip, sn.ip, "0.0.0.0")) updated = true;
if (update_if_changed(snode.port, sn.port, 0)) updated = true;
if (update_if_changed(snode.omq_port, sn.omq_port, 0)) updated = true;
if (updated)
updates_count++;
}
}
}
OXEN_LOG(debug, "Updated {} entries from oxend", updates_count);
return result_swarms;
}
void Swarm::apply_swarm_changes(const all_swarms_t& new_swarms) {
OXEN_LOG(trace, "Applying swarm changes");
all_valid_swarms_ = apply_ips(new_swarms, all_valid_swarms_);
}
void Swarm::update_state(const all_swarms_t& swarms,
const std::vector<sn_record_t>& decommissioned,
const SwarmEvents& events, bool active) {
if (active) {
// The following only makes sense for active nodes in a swarm
if (events.dissolved) {
OXEN_LOG(info, "EVENT: our old swarm got DISSOLVED!");
}
for (const sn_record_t& sn : events.new_snodes) {
OXEN_LOG(info, "EVENT: detected new SN: {}", sn.pubkey_legacy);
}
for (swarm_id_t swarm : events.new_swarms) {
OXEN_LOG(info, "EVENT: detected a new swarm: {}", swarm);
}
apply_swarm_changes(swarms);
const auto& members = events.our_swarm_members;
/// sanity check
if (members.empty())
return;
swarm_peers_.clear();
swarm_peers_.reserve(members.size() - 1);
std::copy_if(members.begin(), members.end(),
std::back_inserter(swarm_peers_),
[this](const sn_record_t& record) {
return record != our_address_;
});
}
// Store a copy of every node in a separate data structure
all_funded_nodes_.clear();
all_funded_ed25519_.clear();
all_funded_x25519_.clear();
for (const auto& si : swarms) {
for (const auto& sn : si.snodes) {
all_funded_nodes_.emplace(sn.pubkey_legacy, sn);
}
}
for (const auto& sn : decommissioned) {
all_funded_nodes_.emplace(sn.pubkey_legacy, sn);
}
for (const auto& [pk, sn] : all_funded_nodes_) {
all_funded_ed25519_.emplace(sn.pubkey_ed25519, pk);
all_funded_x25519_.emplace(sn.pubkey_x25519, pk);
}
}
std::optional<sn_record_t>
Swarm::find_node(const legacy_pubkey& pk) const {
if (auto it = all_funded_nodes_.find(pk); it != all_funded_nodes_.end())
return it->second;
return std::nullopt;
}
std::optional<sn_record_t>
Swarm::find_node(const ed25519_pubkey& pk) const {
if (auto it = all_funded_ed25519_.find(pk); it != all_funded_ed25519_.end())
return find_node(it->second);
return std::nullopt;
}
std::optional<sn_record_t>
Swarm::find_node(const x25519_pubkey& pk) const {
if (auto it = all_funded_x25519_.find(pk); it != all_funded_x25519_.end())
return find_node(it->second);
return std::nullopt;
}
static uint64_t hex_to_u64(const user_pubkey_t& pk) {
/// Create a buffer for 16 characters null terminated
char buf[17] = {};
const auto hexpk = pk.key();
assert(hexpk.size() == 64 && oxenmq::is_hex(hexpk));
uint64_t res = 0;
for (size_t i = 0; i < 64; i += 16) {
std::memcpy(buf, hexpk.data() + i, 16);
res ^= strtoull(buf, nullptr, 16);
}
return res;
}
bool Swarm::is_pubkey_for_us(const user_pubkey_t& pk) const {
/// TODO: Make sure no exceptions bubble up from here!
return cur_swarm_id_ == get_swarm_by_pk(all_valid_swarms_, pk).swarm_id;
}
static const SwarmInfo null_swarm{INVALID_SWARM_ID, {}};
const SwarmInfo& get_swarm_by_pk(
const std::vector<SwarmInfo>& all_swarms,
const user_pubkey_t& pk) {
const uint64_t res = hex_to_u64(pk);
/// We reserve UINT64_MAX as a sentinel swarm id for unassigned snodes
constexpr swarm_id_t MAX_ID = INVALID_SWARM_ID - 1;
const SwarmInfo* cur_best = &null_swarm;
uint64_t cur_min = INVALID_SWARM_ID;
/// We don't require that all_swarms is sorted, so we find
/// the smallest/largest elements in the same loop
const SwarmInfo* leftmost = &null_swarm;
const SwarmInfo* rightmost = nullptr;
for (const auto& si : all_swarms) {
if (si.swarm_id == INVALID_SWARM_ID) {
/// Just to be sure we check again that no decomissioned
/// node is exposed to clients
continue;
}
uint64_t dist =
(si.swarm_id > res) ? (si.swarm_id - res) : (res - si.swarm_id);
if (dist < cur_min) {
cur_best = &si;
cur_min = dist;
}
/// Find the letfmost
if (si.swarm_id < leftmost->swarm_id) {
leftmost = &si;
}
if (!rightmost || si.swarm_id > rightmost->swarm_id) {
rightmost = &si;
}
}
if (!rightmost) // Found no swarms at all
return null_swarm;
// handle special case
if (res > rightmost->swarm_id) {
// since rightmost is at least as large as leftmost,
// res >= leftmost_id in this branch, so the value will
// not overflow; the same logic applies to the else branch
const uint64_t dist = (MAX_ID - res) + leftmost->swarm_id;
if (dist < cur_min) {
cur_best = leftmost;
}
} else if (res < leftmost->swarm_id) {
const uint64_t dist = res + (MAX_ID - rightmost->swarm_id);
if (dist < cur_min) {
cur_best = rightmost;
}
}
return *cur_best;
}
std::pair<int, int> count_missing_data(const block_update_t& bu) {
auto result = std::make_pair(0, 0);
auto& [missing, total] = result;
for (auto& swarm : bu.swarms) {
for (auto& snode : swarm.snodes) {
total++;
if (snode.ip.empty() || snode.ip == "0.0.0.0" || !snode.port || !snode.omq_port ||
!snode.pubkey_ed25519 || !snode.pubkey_x25519)
{ OXEN_LOG(warn, "well wtf {} {} {} {} {}",
snode.ip, snode.port, snode.omq_port, snode.pubkey_ed25519, snode.pubkey_x25519);
missing++;
}
}
}
return result;
}
} // namespace oxen