mirror of https://github.com/oxen-io/oxen-core.git
3076 lines
136 KiB
C++
3076 lines
136 KiB
C++
// Copyright (c) 2018-2020, The Loki Project
|
|
// Copyright (c) 2014-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.
|
|
//
|
|
// Parts of this file are originally copyright (c) 2012-2013 The Cryptonote developers
|
|
|
|
#include <boost/program_options/options_description.hpp>
|
|
#include <boost/program_options/variables_map.hpp>
|
|
#include <algorithm>
|
|
#include <cstring>
|
|
#include <iterator>
|
|
#include <type_traits>
|
|
#include <variant>
|
|
#include <oxenc/base64.h>
|
|
#include "common/json_binary_proxy.h"
|
|
#include "common/string_util.h"
|
|
#include "crypto/crypto.h"
|
|
#include "cryptonote_basic/hardfork.h"
|
|
#include "cryptonote_basic/tx_extra.h"
|
|
#include "cryptonote_config.h"
|
|
#include "cryptonote_core/oxen_name_system.h"
|
|
#include "cryptonote_core/pulse.h"
|
|
#include "cryptonote_core/service_node_rules.h"
|
|
#include "epee/net/network_throttle.hpp"
|
|
#include "oxen/log.hpp"
|
|
#include "oxen_economy.h"
|
|
#include "epee/string_tools.h"
|
|
#include "core_rpc_server.h"
|
|
#include "core_rpc_server_binary_commands.h"
|
|
#include "core_rpc_server_command_parser.h"
|
|
#include "core_rpc_server_error_codes.h"
|
|
#include "rpc/common/rpc_args.h"
|
|
#include "common/command_line.h"
|
|
#include "common/oxen.h"
|
|
#include "common/sha256sum.h"
|
|
#include "common/random.h"
|
|
#include "common/hex.h"
|
|
#include "cryptonote_basic/cryptonote_format_utils.h"
|
|
#include "cryptonote_basic/account.h"
|
|
#include "cryptonote_basic/cryptonote_basic_impl.h"
|
|
#include "cryptonote_core/tx_sanity_check.h"
|
|
#include "cryptonote_core/uptime_proof.h"
|
|
#include "net/parse.h"
|
|
#include "crypto/hash.h"
|
|
#include "p2p/net_node.h"
|
|
#include "serialization/json_archive.h"
|
|
#include "version.h"
|
|
|
|
#include "rpc/common/json_bt.h"
|
|
#include "rpc/common/rpc_command.h"
|
|
#include <fmt/core.h>
|
|
#include <fmt/color.h>
|
|
|
|
namespace cryptonote::rpc {
|
|
|
|
using nlohmann::json;
|
|
using tools::json_binary_proxy;
|
|
|
|
static auto logcat = log::Cat("daemon.rpc");
|
|
|
|
namespace {
|
|
|
|
template <typename RPC>
|
|
void register_rpc_command(std::unordered_map<std::string, std::shared_ptr<const rpc_command>>& regs)
|
|
{
|
|
static_assert(std::is_base_of_v<RPC_COMMAND, RPC> && !std::is_base_of_v<BINARY, RPC>);
|
|
auto cmd = std::make_shared<rpc_command>();
|
|
cmd->is_public = std::is_base_of_v<PUBLIC, RPC>;
|
|
cmd->is_legacy = std::is_base_of_v<LEGACY, RPC>;
|
|
|
|
// Temporary: remove once RPC conversion is complete
|
|
static_assert(!FIXME_has_nested_response_v<RPC>);
|
|
|
|
cmd->invoke = make_invoke<RPC, core_rpc_server, rpc_command>();
|
|
|
|
for (const auto& name : RPC::names())
|
|
regs.emplace(name, cmd);
|
|
}
|
|
|
|
template <typename RPC>
|
|
void register_binary_rpc_command(std::unordered_map<std::string, std::shared_ptr<const rpc_command>>& regs)
|
|
{
|
|
static_assert(std::is_base_of_v<BINARY, RPC> && !std::is_base_of_v<LEGACY, RPC>);
|
|
auto cmd = std::make_shared<rpc_command>();
|
|
cmd->is_public = std::is_base_of_v<PUBLIC, RPC>;
|
|
cmd->is_binary = true;
|
|
|
|
// Legacy binary request; these still use epee serialization, and should be considered
|
|
// deprecated (tentatively to be removed in Oxen 11).
|
|
cmd->invoke = [](rpc_request&& request, core_rpc_server& server) -> rpc_command::result_type {
|
|
typename RPC::request req{};
|
|
std::string_view data;
|
|
if (auto body = request.body_view())
|
|
data = *body;
|
|
else
|
|
throw std::runtime_error{"Internal error: can't load binary a RPC command with non-string body"};
|
|
if (!epee::serialization::load_t_from_binary(req, data))
|
|
throw parse_error{"Failed to parse binary data parameters"};
|
|
|
|
auto res = server.invoke(std::move(req), std::move(request.context));
|
|
|
|
std::string response;
|
|
epee::serialization::store_t_to_binary(res, response);
|
|
return response;
|
|
};
|
|
|
|
for (const auto& name : RPC::names())
|
|
regs.emplace(name, cmd);
|
|
}
|
|
|
|
template <typename... RPC, typename... BinaryRPC>
|
|
std::unordered_map<std::string, std::shared_ptr<const rpc_command>> register_rpc_commands(tools::type_list<RPC...>, tools::type_list<BinaryRPC...>) {
|
|
std::unordered_map<std::string, std::shared_ptr<const rpc_command>> regs;
|
|
|
|
(register_rpc_command<RPC>(regs), ...);
|
|
(register_binary_rpc_command<BinaryRPC>(regs), ...);
|
|
|
|
return regs;
|
|
}
|
|
|
|
constexpr uint64_t OUTPUT_HISTOGRAM_RECENT_CUTOFF_RESTRICTION = 3 * 86400; // 3 days max, the wallet requests 1.8 days
|
|
constexpr uint64_t round_up(uint64_t value, uint64_t quantum) { return (value + quantum - 1) / quantum * quantum; }
|
|
|
|
}
|
|
|
|
const std::unordered_map<std::string, std::shared_ptr<const rpc_command>> rpc_commands = register_rpc_commands(rpc::core_rpc_types{}, rpc::core_rpc_binary_types{});
|
|
|
|
//-----------------------------------------------------------------------------------
|
|
void core_rpc_server::init_options(boost::program_options::options_description& desc, boost::program_options::options_description& hidden)
|
|
{
|
|
cryptonote::rpc_args::init_options(desc, hidden);
|
|
}
|
|
//------------------------------------------------------------------------------------------------------------------------------
|
|
core_rpc_server::core_rpc_server(
|
|
core& cr
|
|
, nodetool::node_server<cryptonote::t_cryptonote_protocol_handler<cryptonote::core> >& p2p
|
|
)
|
|
: m_core(cr)
|
|
, m_p2p(p2p)
|
|
{}
|
|
//------------------------------------------------------------------------------------------------------------------------------
|
|
bool core_rpc_server::check_core_ready()
|
|
{
|
|
return m_p2p.get_payload_object().is_synchronized();
|
|
}
|
|
|
|
|
|
#define CHECK_CORE_READY() do { if(!check_core_ready()){ res.status = STATUS_BUSY; return res; } } while(0)
|
|
|
|
//------------------------------------------------------------------------------------------------------------------------------
|
|
void core_rpc_server::invoke(GET_HEIGHT& get_height, rpc_context context)
|
|
{
|
|
auto [height, hash] = m_core.get_blockchain_top();
|
|
|
|
++height; // block height to chain height
|
|
get_height.response["status"] = STATUS_OK;
|
|
get_height.response["height"] = height;
|
|
get_height.response_hex["hash"] = hash;
|
|
|
|
uint64_t immutable_height = 0;
|
|
cryptonote::checkpoint_t checkpoint;
|
|
if (m_core.get_blockchain_storage().get_db().get_immutable_checkpoint(&checkpoint, height - 1))
|
|
{
|
|
get_height.response["immutable_height"] = checkpoint.height;
|
|
get_height.response_hex["immutable_hash"] = checkpoint.block_hash;
|
|
}
|
|
}
|
|
//------------------------------------------------------------------------------------------------------------------------------
|
|
void core_rpc_server::invoke(GET_INFO& info, rpc_context context)
|
|
{
|
|
|
|
auto [top_height, top_hash] = m_core.get_blockchain_top();
|
|
|
|
auto& bs = m_core.get_blockchain_storage();
|
|
auto& db = bs.get_db();
|
|
|
|
auto prev_ts = db.get_block_timestamp(top_height);
|
|
auto height = top_height + 1; // turn top block height into blockchain height
|
|
|
|
info.response["height"] = height;
|
|
info.response_hex["top_block_hash"] = top_hash;
|
|
info.response["target_height"] = m_core.get_target_blockchain_height();
|
|
|
|
info.response["hard_fork"] = m_core.get_blockchain_storage().get_network_version();
|
|
|
|
bool next_block_is_pulse = false;
|
|
if (pulse::timings t;
|
|
pulse::get_round_timings(bs, height, prev_ts, t)) {
|
|
info.response["pulse_ideal_timestamp"] = tools::to_seconds(t.ideal_timestamp.time_since_epoch());
|
|
info.response["pulse_target_timestamp"] = tools::to_seconds(t.r0_timestamp.time_since_epoch());
|
|
next_block_is_pulse = pulse::clock::now() < t.miner_fallback_timestamp;
|
|
}
|
|
|
|
if (cryptonote::checkpoint_t checkpoint;
|
|
db.get_immutable_checkpoint(&checkpoint, top_height))
|
|
{
|
|
info.response["immutable_height"] = checkpoint.height;
|
|
info.response_hex["immutable_block_hash"] = checkpoint.block_hash;
|
|
}
|
|
|
|
if (next_block_is_pulse)
|
|
info.response["pulse"] = true;
|
|
else
|
|
info.response["difficulty"] = bs.get_difficulty_for_next_block(next_block_is_pulse);
|
|
|
|
info.response["target"] = tools::to_seconds(TARGET_BLOCK_TIME);
|
|
// This count seems broken: blocks with no outputs (after batching) shouldn't be subtracted, and
|
|
// 0-output txes (SN state changes) arguably shouldn't be, either.
|
|
info.response["tx_count"] = m_core.get_blockchain_storage().get_total_transactions() - height; //without coinbase
|
|
info.response["tx_pool_size"] = m_core.get_pool().get_transactions_count();
|
|
if (context.admin)
|
|
{
|
|
info.response["alt_blocks_count"] = bs.get_alternative_blocks_count();
|
|
auto total_conn = m_p2p.get_public_connections_count();
|
|
auto outgoing_conns = m_p2p.get_public_outgoing_connections_count();
|
|
info.response["outgoing_connections_count"] = outgoing_conns;
|
|
info.response["incoming_connections_count"] = total_conn - outgoing_conns;
|
|
info.response["white_peerlist_size"] = m_p2p.get_public_white_peers_count();
|
|
info.response["grey_peerlist_size"] = m_p2p.get_public_gray_peers_count();
|
|
}
|
|
|
|
cryptonote::network_type nettype = m_core.get_nettype();
|
|
info.response["mainnet"] = nettype == network_type::MAINNET;
|
|
if (nettype == network_type::TESTNET) info.response["testnet"] = true;
|
|
else if (nettype == network_type::DEVNET) info.response["devnet"] = true;
|
|
else if (nettype != network_type::MAINNET) info.response["fakechain"] = true;
|
|
info.response["nettype"] = nettype == network_type::MAINNET ? "mainnet" : nettype == network_type::TESTNET ? "testnet" : nettype == network_type::DEVNET ? "devnet" : "fakechain";
|
|
|
|
try
|
|
{
|
|
auto cd = db.get_block_cumulative_difficulty(top_height);
|
|
info.response["cumulative_difficulty"] = cd;
|
|
}
|
|
catch(std::exception const &e)
|
|
{
|
|
info.response["status"] = "Error retrieving cumulative difficulty at height " + std::to_string(top_height);
|
|
return;
|
|
}
|
|
|
|
info.response["block_size_limit"] = bs.get_current_cumulative_block_weight_limit();
|
|
info.response["block_size_median"] = bs.get_current_cumulative_block_weight_median();
|
|
|
|
auto ons_counts = bs.name_system_db().get_mapping_counts(height);
|
|
info.response["ons_counts"] = std::array{
|
|
ons_counts[ons::mapping_type::session],
|
|
ons_counts[ons::mapping_type::wallet],
|
|
ons_counts[ons::mapping_type::lokinet]};
|
|
|
|
if (context.admin)
|
|
{
|
|
bool sn = m_core.service_node();
|
|
info.response["service_node"] = sn;
|
|
info.response["start_time"] = m_core.get_start_time();
|
|
if (sn) {
|
|
info.response["last_storage_server_ping"] = m_core.m_last_storage_server_ping.load();
|
|
info.response["last_lokinet_ping"] = m_core.m_last_lokinet_ping.load();
|
|
}
|
|
info.response["free_space"] = m_core.get_free_space();
|
|
}
|
|
|
|
if (m_core.offline())
|
|
info.response["offline"] = true;
|
|
auto db_size = db.get_database_size();
|
|
info.response["database_size"] = context.admin ? db_size : round_up(db_size, 1'000'000'000);
|
|
info.response["version"] = context.admin ? OXEN_VERSION_FULL : std::to_string(OXEN_VERSION[0]);
|
|
info.response["status_line"] = context.admin ? m_core.get_status_string() :
|
|
"v" + std::to_string(OXEN_VERSION[0]) + "; Height: " + std::to_string(height);
|
|
|
|
info.response["status"] = STATUS_OK;
|
|
}
|
|
//------------------------------------------------------------------------------------------------------------------------------
|
|
void core_rpc_server::invoke(GET_NET_STATS& get_net_stats, rpc_context context)
|
|
{
|
|
get_net_stats.response["start_time"] = m_core.get_start_time();
|
|
{
|
|
std::lock_guard lock{epee::net_utils::network_throttle_manager::m_lock_get_global_throttle_in};
|
|
auto [packets, bytes] = epee::net_utils::network_throttle_manager::get_global_throttle_in().get_stats();
|
|
get_net_stats.response["total_packets_in"] = packets;
|
|
get_net_stats.response["total_bytes_in"] = bytes;
|
|
}
|
|
{
|
|
std::lock_guard lock{epee::net_utils::network_throttle_manager::m_lock_get_global_throttle_out};
|
|
auto [packets, bytes] = epee::net_utils::network_throttle_manager::get_global_throttle_out().get_stats();
|
|
get_net_stats.response["total_packets_in"] = packets;
|
|
get_net_stats.response["total_bytes_in"] = bytes;
|
|
}
|
|
get_net_stats.response["status"] = STATUS_OK;
|
|
}
|
|
namespace {
|
|
//------------------------------------------------------------------------------------------------------------------------------
|
|
class pruned_transaction {
|
|
transaction& tx;
|
|
public:
|
|
pruned_transaction(transaction& tx) : tx(tx) {}
|
|
BEGIN_SERIALIZE_OBJECT()
|
|
tx.serialize_base(ar);
|
|
END_SERIALIZE()
|
|
};
|
|
}
|
|
//------------------------------------------------------------------------------------------------------------------------------
|
|
GET_BLOCKS_BIN::response core_rpc_server::invoke(GET_BLOCKS_BIN::request&& req, rpc_context context)
|
|
{
|
|
GET_BLOCKS_BIN::response res{};
|
|
|
|
std::vector<std::pair<std::pair<std::string, crypto::hash>, std::vector<std::pair<crypto::hash, std::string> > > > bs;
|
|
|
|
if(!m_core.find_blockchain_supplement(req.start_height, req.block_ids, bs, res.current_height, res.start_height, req.prune, !req.no_miner_tx, GET_BLOCKS_BIN::MAX_COUNT))
|
|
{
|
|
res.status = "Failed";
|
|
return res;
|
|
}
|
|
|
|
size_t size = 0, ntxes = 0;
|
|
res.blocks.reserve(bs.size());
|
|
res.output_indices.reserve(bs.size());
|
|
for(auto& bd: bs)
|
|
{
|
|
res.blocks.resize(res.blocks.size()+1);
|
|
res.blocks.back().block = bd.first.first;
|
|
size += bd.first.first.size();
|
|
res.output_indices.push_back(GET_BLOCKS_BIN::block_output_indices());
|
|
ntxes += bd.second.size();
|
|
res.output_indices.back().indices.reserve(1 + bd.second.size());
|
|
if (req.no_miner_tx)
|
|
res.output_indices.back().indices.push_back(GET_BLOCKS_BIN::tx_output_indices());
|
|
res.blocks.back().txs.reserve(bd.second.size());
|
|
for (auto& [txhash, txdata] : bd.second)
|
|
{
|
|
size += res.blocks.back().txs.emplace_back(std::move(txdata)).size();
|
|
}
|
|
|
|
const size_t n_txes_to_lookup = bd.second.size() + (req.no_miner_tx ? 0 : 1);
|
|
if (n_txes_to_lookup > 0)
|
|
{
|
|
std::vector<std::vector<uint64_t>> indices;
|
|
bool r = m_core.get_tx_outputs_gindexs(req.no_miner_tx ? bd.second.front().first : bd.first.second, n_txes_to_lookup, indices);
|
|
if (!r || indices.size() != n_txes_to_lookup || res.output_indices.back().indices.size() != (req.no_miner_tx ? 1 : 0))
|
|
{
|
|
res.status = "Failed";
|
|
return res;
|
|
}
|
|
for (size_t i = 0; i < indices.size(); ++i)
|
|
res.output_indices.back().indices.push_back({std::move(indices[i])});
|
|
}
|
|
}
|
|
|
|
log::debug(logcat, "on_get_blocks: {} blocks, {} txes, size {}", bs.size(), ntxes, size);
|
|
res.status = STATUS_OK;
|
|
return res;
|
|
}
|
|
GET_ALT_BLOCKS_HASHES_BIN::response core_rpc_server::invoke(GET_ALT_BLOCKS_HASHES_BIN::request&& req, rpc_context context)
|
|
{
|
|
GET_ALT_BLOCKS_HASHES_BIN::response res{};
|
|
|
|
std::vector<block> blks;
|
|
|
|
if(!m_core.get_alternative_blocks(blks))
|
|
{
|
|
res.status = "Failed";
|
|
return res;
|
|
}
|
|
|
|
res.blks_hashes.reserve(blks.size());
|
|
|
|
for (auto const& blk: blks)
|
|
{
|
|
res.blks_hashes.push_back(tools::type_to_hex(get_block_hash(blk)));
|
|
}
|
|
|
|
log::debug(logcat, "on_get_alt_blocks_hashes: {} blocks ", blks.size());
|
|
res.status = STATUS_OK;
|
|
return res;
|
|
}
|
|
//------------------------------------------------------------------------------------------------------------------------------
|
|
GET_BLOCKS_BY_HEIGHT_BIN::response core_rpc_server::invoke(GET_BLOCKS_BY_HEIGHT_BIN::request&& req, rpc_context context)
|
|
{
|
|
GET_BLOCKS_BY_HEIGHT_BIN::response res{};
|
|
|
|
res.status = "Failed";
|
|
res.blocks.clear();
|
|
res.blocks.reserve(req.heights.size());
|
|
for (uint64_t height : req.heights)
|
|
{
|
|
block blk;
|
|
try
|
|
{
|
|
blk = m_core.get_blockchain_storage().get_db().get_block_from_height(height);
|
|
}
|
|
catch (...)
|
|
{
|
|
res.status = "Error retrieving block at height " + std::to_string(height);
|
|
return res;
|
|
}
|
|
std::vector<transaction> txs;
|
|
m_core.get_transactions(blk.tx_hashes, txs);
|
|
res.blocks.resize(res.blocks.size() + 1);
|
|
res.blocks.back().block = block_to_blob(blk);
|
|
for (auto& tx : txs)
|
|
res.blocks.back().txs.push_back(tx_to_blob(tx));
|
|
}
|
|
res.status = STATUS_OK;
|
|
return res;
|
|
}
|
|
//------------------------------------------------------------------------------------------------------------------------------
|
|
GET_HASHES_BIN::response core_rpc_server::invoke(GET_HASHES_BIN::request&& req, rpc_context context)
|
|
{
|
|
GET_HASHES_BIN::response res{};
|
|
|
|
res.start_height = req.start_height;
|
|
if(!m_core.get_blockchain_storage().find_blockchain_supplement(req.block_ids, res.m_block_ids, res.start_height, res.current_height, false))
|
|
{
|
|
res.status = "Failed";
|
|
return res;
|
|
}
|
|
|
|
res.status = STATUS_OK;
|
|
return res;
|
|
}
|
|
//------------------------------------------------------------------------------------------------------------------------------
|
|
GET_OUTPUTS_BIN::response core_rpc_server::invoke(GET_OUTPUTS_BIN::request&& req, rpc_context context)
|
|
{
|
|
GET_OUTPUTS_BIN::response res{};
|
|
|
|
if (!context.admin && req.outputs.size() > GET_OUTPUTS_BIN::MAX_COUNT)
|
|
res.status = "Too many outs requested";
|
|
else if (m_core.get_outs(req, res))
|
|
res.status = STATUS_OK;
|
|
else
|
|
res.status = "Failed";
|
|
|
|
return res;
|
|
}
|
|
//------------------------------------------------------------------------------------------------------------------------------
|
|
void core_rpc_server::invoke(GET_OUTPUTS& get_outputs, rpc_context context)
|
|
{
|
|
if (!context.admin && get_outputs.request.output_indices.size() > GET_OUTPUTS::MAX_COUNT) {
|
|
get_outputs.response["status"] = "Too many outs requested";
|
|
return;
|
|
}
|
|
|
|
// This is nasty. WTF are core methods taking *local rpc* types?
|
|
// FIXME: make core methods take something sensible, like a std::vector<uint64_t>. (We really
|
|
// don't need the pair since amount is also 0 for Oxen since the beginning of the chain; only in
|
|
// ancient Monero blocks was it non-zero).
|
|
GET_OUTPUTS_BIN::request req_bin{};
|
|
req_bin.get_txid = get_outputs.request.get_txid;
|
|
req_bin.outputs.reserve(get_outputs.request.output_indices.size());
|
|
for (auto oi : get_outputs.request.output_indices)
|
|
req_bin.outputs.push_back({0, oi});
|
|
|
|
GET_OUTPUTS_BIN::response res_bin{};
|
|
if (!m_core.get_outs(req_bin, res_bin))
|
|
{
|
|
get_outputs.response["status"] = STATUS_FAILED;
|
|
return;
|
|
}
|
|
|
|
auto binary_format = get_outputs.is_bt() ? json_binary_proxy::fmt::bt : json_binary_proxy::fmt::hex;
|
|
|
|
auto& outs = (get_outputs.response["outs"] = json::array());
|
|
if (!get_outputs.request.as_tuple) {
|
|
for (auto& outkey : res_bin.outs) {
|
|
json o;
|
|
json_binary_proxy b{o, binary_format};
|
|
b["key"] = std::move(outkey.key);
|
|
b["mask"] = std::move(outkey.mask);
|
|
o["unlocked"] = outkey.unlocked;
|
|
o["height"] = outkey.height;
|
|
if (get_outputs.request.get_txid)
|
|
b["txid"] = std::move(outkey.txid);
|
|
outs.push_back(std::move(o));
|
|
}
|
|
} else {
|
|
for (auto& outkey : res_bin.outs) {
|
|
auto o = json::array();
|
|
json_binary_proxy b{o, binary_format};
|
|
b.push_back(std::move(outkey.key));
|
|
b.push_back(std::move(outkey.mask));
|
|
o.push_back(outkey.unlocked);
|
|
o.push_back(outkey.height);
|
|
if (get_outputs.request.get_txid)
|
|
b.push_back(std::move(outkey.txid));
|
|
outs.push_back(o);
|
|
}
|
|
}
|
|
|
|
get_outputs.response["status"] = STATUS_OK;
|
|
}
|
|
//------------------------------------------------------------------------------------------------------------------------------
|
|
GET_TX_GLOBAL_OUTPUTS_INDEXES_BIN::response core_rpc_server::invoke(GET_TX_GLOBAL_OUTPUTS_INDEXES_BIN::request&& req, rpc_context context)
|
|
{
|
|
GET_TX_GLOBAL_OUTPUTS_INDEXES_BIN::response res{};
|
|
|
|
bool r = m_core.get_tx_outputs_gindexs(req.txid, res.o_indexes);
|
|
if(!r)
|
|
{
|
|
res.status = "Failed";
|
|
return res;
|
|
}
|
|
res.status = STATUS_OK;
|
|
log::debug(logcat, "GET_TX_GLOBAL_OUTPUTS_INDEXES: [{}]", res.o_indexes.size());
|
|
return res;
|
|
}
|
|
|
|
namespace {
|
|
constexpr uint64_t half_microportion = 9223372036855ULL; // half of 1/1'000'000 of a full portion
|
|
constexpr uint32_t microportion(uint64_t portion) {
|
|
// Rounding integer division to convert our [0, ..., 2^64-4] portion value into [0, ..., 1000000]:
|
|
return portion < half_microportion ? 0 : (portion - half_microportion) / (2*half_microportion) + 1;
|
|
}
|
|
template <typename T>
|
|
std::vector<std::string> hexify(const std::vector<T>& v) {
|
|
std::vector<std::string> hexes;
|
|
hexes.reserve(v.size());
|
|
for (auto& x : v)
|
|
hexes.push_back(tools::type_to_hex(x));
|
|
return hexes;
|
|
}
|
|
|
|
struct extra_extractor {
|
|
nlohmann::json& entry;
|
|
const network_type nettype;
|
|
json_binary_proxy::fmt format;
|
|
|
|
// If we encounter duplicate values then we want to produce an array of values, but with just
|
|
// a single one we want just the value itself; this does that. Returns a reference to the
|
|
// assigned value (whether as a top-level value or array element).
|
|
template <typename T>
|
|
json& set(
|
|
const std::string& key,
|
|
T&& value,
|
|
[[maybe_unused]] bool binary = tools::json_is_binary<T> || tools::json_is_binary_container<T>) {
|
|
auto* x = &entry[key];
|
|
if (!x->is_null() && !x->is_array())
|
|
x = &(entry[key] = json::array({std::move(*x)}));
|
|
if (x->is_array())
|
|
x = &x->emplace_back();
|
|
if constexpr (tools::json_is_binary<T> || tools::json_is_binary_container<T> || std::is_convertible_v<T, std::string_view>) {
|
|
if (binary)
|
|
return json_binary_proxy{*x, format} = std::forward<T>(value);
|
|
}
|
|
assert(!binary);
|
|
return *x = std::forward<T>(value);
|
|
}
|
|
|
|
void operator()(const tx_extra_pub_key& x) { set("pubkey", x.pub_key); }
|
|
void operator()(const tx_extra_nonce& x) {
|
|
if ((x.nonce.size() == sizeof(crypto::hash) + 1 && x.nonce[0] == TX_EXTRA_NONCE_PAYMENT_ID)
|
|
|| (x.nonce.size() == sizeof(crypto::hash8) + 1 && x.nonce[0] == TX_EXTRA_NONCE_ENCRYPTED_PAYMENT_ID))
|
|
set("payment_id", std::string_view{x.nonce.data() + 1, x.nonce.size() - 1}, true);
|
|
else
|
|
set("extra_nonce", x.nonce, true);
|
|
}
|
|
void operator()(const tx_extra_merge_mining_tag& x) { set("mm_depth", x.depth); set("mm_root", x.merkle_root); }
|
|
void operator()(const tx_extra_additional_pub_keys& x) { set("additional_pubkeys", x.data); }
|
|
void operator()(const tx_extra_burn& x) { set("burn_amount", x.amount); }
|
|
void operator()(const tx_extra_service_node_winner& x) { set("sn_winner", x.m_service_node_key); }
|
|
void operator()(const tx_extra_service_node_pubkey& x) { set("sn_pubkey", x.m_service_node_key); }
|
|
void operator()(const tx_extra_service_node_register& x) {
|
|
//MERGEFIX: confirm this is correct
|
|
json new_reg{};
|
|
if (x.hf_or_expiration <= 255) { // hard fork value
|
|
new_reg["hardfork"] = static_cast<hf>(x.hf_or_expiration);
|
|
new_reg["fee"] = x.fee * 1'000'000 / STAKING_FEE_BASIS;
|
|
} else { // timestamp
|
|
new_reg["hardfork"] = hf::none;
|
|
new_reg["expiry"] = x.hf_or_expiration;
|
|
new_reg["fee"] = microportion(x.fee);
|
|
}
|
|
hf reg_hf = new_reg["hardfork"];
|
|
|
|
new_reg["contributors"] = json::array();
|
|
for (size_t i = 0; i < x.amounts.size(); i++) {
|
|
auto wallet = get_account_address_as_str(nettype, false, {x.public_spend_keys[i], x.public_view_keys[i]});
|
|
uint64_t amount;
|
|
uint32_t portion;
|
|
if (reg_hf >= hf::hf19_reward_batching) {
|
|
amount = x.amounts[i];
|
|
// We aren't given info on whether this is testnet/mainnet, but we can guess by looking
|
|
// at the operator amount, which has to be <= 100 on testnet, but >= 3750 on mainnet.
|
|
auto nettype = x.amounts[0] > oxen::STAKING_REQUIREMENT_TESTNET
|
|
? network_type::MAINNET : network_type::TESTNET;
|
|
portion = std::lround(amount / (double) service_nodes::get_staking_requirement(nettype, reg_hf) * 1'000'000.0);
|
|
} else {
|
|
amount = 0;
|
|
portion = microportion(x.amounts[i]);
|
|
}
|
|
new_reg["contributors"].push_back(json{
|
|
{"wallet", std::move(wallet)},
|
|
{"amount", amount},
|
|
{"portion", portion}});
|
|
}
|
|
set("sn_registration", std::move(new_reg));
|
|
}
|
|
void operator()(const tx_extra_service_node_contributor& x) {
|
|
set("sn_contributor", get_account_address_as_str(nettype, false, {x.m_spend_public_key, x.m_view_public_key}));
|
|
}
|
|
template <typename T>
|
|
auto& _state_change(const T& x) {
|
|
// Common loading code for nearly-identical state_change and deregister_old variables:
|
|
auto voters = json::array();
|
|
for (auto& v : x.votes)
|
|
voters.push_back(v.validator_index);
|
|
|
|
json sc{
|
|
{"height", x.block_height},
|
|
{"index", x.service_node_index},
|
|
{"voters", std::move(voters)}};
|
|
return set("sn_state_change", std::move(sc));
|
|
}
|
|
void operator()(const tx_extra_service_node_deregister_old& x) {
|
|
auto& sc = _state_change(x);
|
|
sc["old_dereg"] = true;
|
|
sc["type"] = "dereg";
|
|
}
|
|
void operator()(const tx_extra_service_node_state_change& x) {
|
|
auto& sc = _state_change(x);
|
|
if (x.reason_consensus_all)
|
|
sc["reasons"] = cryptonote::coded_reasons(x.reason_consensus_all);
|
|
// If `any` has reasons not included in all then list the extra ones separately:
|
|
if (uint16_t reasons_maybe = x.reason_consensus_any & ~x.reason_consensus_all)
|
|
sc["reasons_maybe"] = cryptonote::coded_reasons(reasons_maybe);
|
|
switch (x.state)
|
|
{
|
|
case service_nodes::new_state::decommission: sc["type"] = "decom"; break;
|
|
case service_nodes::new_state::recommission: sc["type"] = "recom"; break;
|
|
case service_nodes::new_state::deregister: sc["type"] = "dereg"; break;
|
|
case service_nodes::new_state::ip_change_penalty: sc["type"] = "ip"; break;
|
|
case service_nodes::new_state::_count: /*leave blank*/ break;
|
|
}
|
|
}
|
|
void operator()(const tx_extra_tx_secret_key& x) { set("tx_secret_key", tools::view_guts(x.key), true); }
|
|
void operator()(const tx_extra_tx_key_image_proofs& x) {
|
|
std::vector<crypto::key_image> kis;
|
|
kis.reserve(x.proofs.size());
|
|
for (auto& proof : x.proofs)
|
|
kis.push_back(proof.key_image);
|
|
set("locked_key_images", std::move(kis));
|
|
}
|
|
void operator()(const tx_extra_tx_key_image_unlock& x) { set("key_image_unlock", x.key_image); }
|
|
void _load_owner(json& parent, const std::string& key, const ons::generic_owner& owner) {
|
|
if (!owner)
|
|
return;
|
|
if (owner.type == ons::generic_owner_sig_type::monero)
|
|
parent[key] = get_account_address_as_str(nettype, owner.wallet.is_subaddress, owner.wallet.address);
|
|
else if (owner.type == ons::generic_owner_sig_type::ed25519)
|
|
json_binary_proxy{parent[key], json_binary_proxy::fmt::hex} = owner.ed25519;
|
|
}
|
|
void operator()(const tx_extra_oxen_name_system& x) {
|
|
json ons{};
|
|
if (auto maybe_exp = ons::expiry_blocks(nettype, x.type))
|
|
ons["blocks"] = *maybe_exp;
|
|
switch (x.type)
|
|
{
|
|
case ons::mapping_type::lokinet: [[fallthrough]];
|
|
case ons::mapping_type::lokinet_2years: [[fallthrough]];
|
|
case ons::mapping_type::lokinet_5years: [[fallthrough]];
|
|
case ons::mapping_type::lokinet_10years: ons["type"] = "lokinet"; break;
|
|
|
|
case ons::mapping_type::session: ons["type"] = "session"; break;
|
|
case ons::mapping_type::wallet: ons["type"] = "wallet"; break;
|
|
|
|
case ons::mapping_type::update_record_internal: [[fallthrough]];
|
|
case ons::mapping_type::_count:
|
|
break;
|
|
}
|
|
if (x.is_buying())
|
|
ons["buy"] = true;
|
|
else if (x.is_updating())
|
|
ons["update"] = true;
|
|
else if (x.is_renewing())
|
|
ons["renew"] = true;
|
|
auto ons_bin = json_binary_proxy{ons, format};
|
|
ons_bin["name_hash"] = x.name_hash;
|
|
if (!x.encrypted_value.empty())
|
|
ons_bin["value"] = x.encrypted_value;
|
|
_load_owner(ons, "owner", x.owner);
|
|
_load_owner(ons, "backup_owner", x.backup_owner);
|
|
}
|
|
|
|
// Ignore these fields:
|
|
void operator()(const tx_extra_padding&) {}
|
|
void operator()(const tx_extra_mysterious_minergate&) {}
|
|
};
|
|
|
|
|
|
void load_tx_extra_data(nlohmann::json& e, const transaction& tx, network_type nettype, bool is_bt)
|
|
{
|
|
e = json::object();
|
|
std::vector<tx_extra_field> extras;
|
|
if (!parse_tx_extra(tx.extra, extras))
|
|
return;
|
|
extra_extractor visitor{e, nettype, is_bt ? json_binary_proxy::fmt::bt : json_binary_proxy::fmt::hex};
|
|
for (const auto& extra : extras)
|
|
var::visit(visitor, extra);
|
|
}
|
|
}
|
|
|
|
struct tx_info {
|
|
txpool_tx_meta_t meta;
|
|
std::string tx_blob; // Blob containing the transaction data.
|
|
bool blink; // True if this is a signed blink transaction
|
|
};
|
|
|
|
static std::unordered_map<crypto::hash, tx_info> get_pool_txs_impl(cryptonote::core& core) {
|
|
auto& bc = core.get_blockchain_storage();
|
|
auto& pool = core.get_pool();
|
|
|
|
std::unordered_map<crypto::hash, tx_info> tx_infos;
|
|
tx_infos.reserve(bc.get_txpool_tx_count());
|
|
|
|
bc.for_all_txpool_txes(
|
|
[&tx_infos, &pool]
|
|
(const crypto::hash& txid, const txpool_tx_meta_t& meta, const std::string* bd) {
|
|
transaction tx;
|
|
if (!parse_and_validate_tx_from_blob(*bd, tx))
|
|
{
|
|
log::error(logcat, "Failed to parse tx from txpool");
|
|
// continue
|
|
return true;
|
|
}
|
|
auto& txi = tx_infos[txid];
|
|
txi.meta = meta;
|
|
txi.tx_blob = *bd;
|
|
tx.set_hash(txid);
|
|
txi.blink = pool.has_blink(txid);
|
|
return true;
|
|
}, true);
|
|
|
|
return tx_infos;
|
|
}
|
|
|
|
static auto pool_locks(cryptonote::core& core) {
|
|
auto& pool = core.get_pool();
|
|
std::unique_lock tx_lock{pool, std::defer_lock};
|
|
std::unique_lock bc_lock{core.get_blockchain_storage(), std::defer_lock};
|
|
auto blink_lock = pool.blink_shared_lock(std::defer_lock);
|
|
std::lock(tx_lock, bc_lock, blink_lock);
|
|
return std::make_tuple(std::move(tx_lock), std::move(bc_lock), std::move(blink_lock));
|
|
}
|
|
|
|
static std::pair<std::unordered_map<crypto::hash, tx_info>, tx_memory_pool::key_images_container> get_pool_txs_kis(cryptonote::core& core) {
|
|
auto locks = pool_locks(core);
|
|
return {get_pool_txs_impl(core), core.get_pool().get_spent_key_images(true)};
|
|
}
|
|
|
|
/*
|
|
static std::unordered_map<crypto::hash, tx_info> get_pool_txs(
|
|
cryptonote::core& core, std::function<void(const transaction&, tx_info&)> post_process = {}) {
|
|
auto locks = pool_locks(core);
|
|
return get_pool_txs_impl(core);
|
|
}
|
|
*/
|
|
|
|
static tx_memory_pool::key_images_container get_pool_kis(
|
|
cryptonote::core& core, std::function<void(const transaction&, tx_info&)> post_process = {}) {
|
|
auto locks = pool_locks(core);
|
|
return core.get_pool().get_spent_key_images(true);
|
|
}
|
|
|
|
//------------------------------------------------------------------------------------------------------------------------------
|
|
void core_rpc_server::invoke(GET_TRANSACTIONS& get, rpc_context context)
|
|
{
|
|
std::unordered_set<crypto::hash> missed_txs;
|
|
using split_tx = std::tuple<crypto::hash, std::string, crypto::hash, std::string>;
|
|
std::vector<split_tx> txs;
|
|
if (!get.request.tx_hashes.empty()) {
|
|
if (!m_core.get_split_transactions_blobs(get.request.tx_hashes, txs, &missed_txs))
|
|
{
|
|
get.response["status"] = STATUS_FAILED;
|
|
return;
|
|
}
|
|
log::debug(logcat, "Found {}/{} transactions on the blockchain", txs.size(), get.request.tx_hashes.size());
|
|
}
|
|
|
|
// try the pool for any missing txes
|
|
auto& pool = m_core.get_pool();
|
|
std::unordered_map<crypto::hash, tx_info> found_in_pool;
|
|
if (!missed_txs.empty() || get.request.memory_pool)
|
|
{
|
|
try {
|
|
auto [pool_txs, pool_kis] = get_pool_txs_kis(m_core);
|
|
|
|
auto split_mempool_tx = [](std::pair<const crypto::hash, tx_info>& info) {
|
|
cryptonote::transaction tx;
|
|
if (!cryptonote::parse_and_validate_tx_from_blob(info.second.tx_blob, tx))
|
|
throw std::runtime_error{"Unable to parse and validate tx from blob"};
|
|
serialization::binary_string_archiver ba;
|
|
try {
|
|
tx.serialize_base(ba);
|
|
} catch (const std::exception& e) {
|
|
throw std::runtime_error{"Failed to serialize transaction base: "s + e.what()};
|
|
}
|
|
std::string pruned = ba.str();
|
|
std::string pruned2{info.second.tx_blob, pruned.size()};
|
|
return split_tx{info.first, std::move(pruned), get_transaction_prunable_hash(tx), std::move(pruned2)};
|
|
};
|
|
|
|
if (!get.request.tx_hashes.empty()) {
|
|
// sort to match original request
|
|
std::vector<split_tx> sorted_txs;
|
|
unsigned txs_processed = 0;
|
|
for (const auto& h: get.request.tx_hashes) {
|
|
if (auto missed_it = missed_txs.find(h); missed_it == missed_txs.end()) {
|
|
if (txs.size() == txs_processed) {
|
|
get.response["status"] = "Failed: internal error - txs is empty";
|
|
return;
|
|
}
|
|
// core returns the ones it finds in the right order
|
|
if (std::get<0>(txs[txs_processed]) != h) {
|
|
get.response["status"] = "Failed: internal error - tx hash mismatch";
|
|
return;
|
|
}
|
|
sorted_txs.push_back(std::move(txs[txs_processed]));
|
|
++txs_processed;
|
|
} else if (auto ptx_it = pool_txs.find(h); ptx_it != pool_txs.end()) {
|
|
sorted_txs.push_back(split_mempool_tx(*ptx_it));
|
|
missed_txs.erase(missed_it);
|
|
found_in_pool.emplace(h, std::move(ptx_it->second));
|
|
}
|
|
}
|
|
txs = std::move(sorted_txs);
|
|
get.response_hex["missed_tx"] = missed_txs; // non-plural here intentional to not break existing clients
|
|
log::debug(logcat, "Found {}/{} transactions in the pool", found_in_pool.size(), get.request.tx_hashes.size());
|
|
} else if (get.request.memory_pool) {
|
|
txs.reserve(pool_txs.size());
|
|
std::transform(pool_txs.begin(), pool_txs.end(), std::back_inserter(txs), split_mempool_tx);
|
|
found_in_pool = std::move(pool_txs);
|
|
|
|
auto mki = get.response_hex["mempool_key_images"];
|
|
for (auto& [ki, txids] : pool_kis) {
|
|
// The *key* is also binary (hex for json):
|
|
std::string key{get.is_bt() ? tools::view_guts(ki) : tools::type_to_hex(ki)};
|
|
mki[key] = txids;
|
|
}
|
|
}
|
|
} catch (const std::exception& e) {
|
|
log::error(logcat, e.what());
|
|
get.response["status"] = "Failed: "s + e.what();
|
|
return;
|
|
}
|
|
}
|
|
|
|
uint64_t immutable_height = m_core.get_blockchain_storage().get_immutable_height();
|
|
auto blink_lock = pool.blink_shared_lock(std::defer_lock); // Defer until/unless we actually need it
|
|
|
|
auto& txs_out = get.response["txs"];
|
|
txs_out = json::array();
|
|
|
|
for (const auto& [tx_hash, unprunable_data, prunable_hash, prunable_data]: txs)
|
|
{
|
|
auto& e = txs_out.emplace_back();
|
|
auto e_bin = get.response_hex["txs"].back();
|
|
e_bin["tx_hash"] = tx_hash;
|
|
e["size"] = unprunable_data.size() + prunable_data.size();
|
|
|
|
// If the transaction was pruned then the prunable part will be empty but the prunable hash
|
|
// will be non-null. (Some txes, like coinbase txes, are non-prunable and will have empty
|
|
// *and* null prunable hash).
|
|
bool prunable = (bool) prunable_hash;
|
|
bool pruned = prunable && prunable_data.empty();
|
|
|
|
if (pruned || (prunable && (get.request.split || get.request.prune)))
|
|
e_bin["prunable_hash"] = prunable_hash;
|
|
|
|
std::string tx_data = unprunable_data;
|
|
if (!get.request.prune)
|
|
tx_data += prunable_data;
|
|
|
|
if (get.request.split || get.request.prune)
|
|
{
|
|
e_bin["pruned"] = unprunable_data;
|
|
if (get.request.split)
|
|
e_bin["prunable"] = prunable_data;
|
|
}
|
|
|
|
if (get.request.data) {
|
|
if (pruned || get.request.prune) {
|
|
if (!e.count("pruned"))
|
|
e_bin["pruned"] = unprunable_data;
|
|
} else {
|
|
e_bin["data"] = tx_data;
|
|
}
|
|
}
|
|
|
|
cryptonote::transaction tx;
|
|
if (get.request.prune || pruned)
|
|
{
|
|
if (!cryptonote::parse_and_validate_tx_base_from_blob(tx_data, tx))
|
|
{
|
|
get.response["status"] = "Failed to parse and validate base tx data";
|
|
return;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
if (!cryptonote::parse_and_validate_tx_from_blob(tx_data, tx))
|
|
{
|
|
get.response["status"] = "Failed to parse and validate tx data";
|
|
return;
|
|
}
|
|
}
|
|
|
|
std::optional<json> extra;
|
|
if (get.request.tx_extra)
|
|
load_tx_extra_data(extra.emplace(), tx, nettype(), get.is_bt());
|
|
if (get.request.tx_extra_raw)
|
|
e_bin["tx_extra_raw"] = std::string_view{reinterpret_cast<const char*>(tx.extra.data()), tx.extra.size()};
|
|
|
|
// Clear it because we don't want/care about it in the RPC output (we already got it more
|
|
// usefully from the above).
|
|
tx.extra.clear();
|
|
|
|
{
|
|
serialization::json_archiver ja{
|
|
get.is_bt() ? json_binary_proxy::fmt::bt : json_binary_proxy::fmt::hex};
|
|
|
|
serialize(ja, tx);
|
|
auto dumped = std::move(ja).json();
|
|
e.update(dumped);
|
|
}
|
|
|
|
if (extra)
|
|
e["extra"] = std::move(*extra);
|
|
else
|
|
e.erase("extra");
|
|
|
|
auto ptx_it = found_in_pool.find(tx_hash);
|
|
bool in_pool = ptx_it != found_in_pool.end();
|
|
auto height = std::numeric_limits<uint64_t>::max();
|
|
|
|
auto hf_version = get_network_version(nettype(), in_pool ? m_core.get_current_blockchain_height() : height);
|
|
if (uint64_t fee, burned; get_tx_miner_fee(tx, fee, hf_version >= feature::FEE_BURNING, &burned)) {
|
|
e["fee"] = fee;
|
|
e["burned"] = burned;
|
|
}
|
|
|
|
if (in_pool)
|
|
{
|
|
e["in_pool"] = true;
|
|
const auto& meta = ptx_it->second.meta;
|
|
e["weight"] = meta.weight;
|
|
e["relayed"] = (bool) ptx_it->second.meta.relayed;
|
|
e["received_timestamp"] = ptx_it->second.meta.receive_time;
|
|
e["blink"] = ptx_it->second.blink;
|
|
if (meta.double_spend_seen) e["double_spend_seen"] = true;
|
|
if (meta.do_not_relay) e["do_not_relay"] = true;
|
|
if (meta.last_relayed_time) e["last_relayed_time"] = meta.last_relayed_time;
|
|
if (meta.kept_by_block) e["kept_by_block"] = (bool) meta.kept_by_block;
|
|
if (meta.last_failed_id) e_bin["last_failed_block"] = meta.last_failed_id;
|
|
if (meta.last_failed_height) e["last_failed_height"] = meta.last_failed_height;
|
|
if (meta.max_used_block_id) e_bin["max_used_block"] = meta.max_used_block_id;
|
|
if (meta.max_used_block_height) e["max_used_height"] = meta.max_used_block_height;
|
|
}
|
|
else
|
|
{
|
|
height = m_core.get_blockchain_storage().get_db().get_tx_block_height(tx_hash);
|
|
e["block_height"] = height;
|
|
e["block_timestamp"] = m_core.get_blockchain_storage().get_db().get_block_timestamp(height);
|
|
if (height > immutable_height) {
|
|
if (!blink_lock) blink_lock.lock();
|
|
e["blink"] = pool.has_blink(tx_hash);
|
|
}
|
|
}
|
|
|
|
{
|
|
service_nodes::staking_components sc;
|
|
if (service_nodes::tx_get_staking_components_and_amounts(nettype(), hf_version, tx, height, &sc)
|
|
&& sc.transferred > 0)
|
|
e["stake_amount"] = sc.transferred;
|
|
}
|
|
|
|
// output indices too if not in pool
|
|
if (!in_pool)
|
|
{
|
|
std::vector<uint64_t> indices;
|
|
if (m_core.get_tx_outputs_gindexs(tx_hash, indices))
|
|
e["output_indices"] = std::move(indices);
|
|
else
|
|
{
|
|
get.response["status"] = STATUS_FAILED;
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
|
|
log::debug(logcat, "{} transactions found, {} not found", get.response["txs"].size(), missed_txs.size());
|
|
get.response["status"] = STATUS_OK;
|
|
}
|
|
//------------------------------------------------------------------------------------------------------------------------------
|
|
void core_rpc_server::invoke(IS_KEY_IMAGE_SPENT& spent, rpc_context context)
|
|
{
|
|
spent.response["status"] = STATUS_FAILED;
|
|
|
|
std::vector<bool> blockchain_spent;
|
|
if (!m_core.are_key_images_spent(spent.request.key_images, blockchain_spent))
|
|
return;
|
|
std::optional<tx_memory_pool::key_images_container> kis;
|
|
auto spent_status = json::array();
|
|
for (size_t n = 0; n < spent.request.key_images.size(); n++) {
|
|
if (blockchain_spent[n])
|
|
spent_status.push_back(IS_KEY_IMAGE_SPENT::SPENT::BLOCKCHAIN);
|
|
else {
|
|
if (!kis) {
|
|
try {
|
|
kis = get_pool_kis(m_core);
|
|
} catch (const std::exception& e) {
|
|
log::error(logcat, "Failed to get pool key images: {}", e.what());
|
|
return;
|
|
}
|
|
}
|
|
spent_status.push_back(kis->count(spent.request.key_images[n])
|
|
? IS_KEY_IMAGE_SPENT::SPENT::POOL : IS_KEY_IMAGE_SPENT::SPENT::UNSPENT);
|
|
}
|
|
}
|
|
|
|
spent.response["status"] = STATUS_OK;
|
|
spent.response["spent_status"] = std::move(spent_status);
|
|
}
|
|
//------------------------------------------------------------------------------------------------------------------------------
|
|
void core_rpc_server::invoke(SUBMIT_TRANSACTION& tx, rpc_context context)
|
|
{
|
|
if (!check_core_ready()) {
|
|
tx.response["status"] = STATUS_BUSY;
|
|
return;
|
|
}
|
|
|
|
if (tx.request.blink)
|
|
{
|
|
auto future = m_core.handle_blink_tx(tx.request.tx);
|
|
// FIXME: blocking here for 10s is nasty; we need to stash this request and come back to it
|
|
// when the blink tx result comes back, and wait for longer (maybe 30s).
|
|
//
|
|
// FIXME 2: on timeout, we should check the mempool to see if it arrived that way so that we
|
|
// return success if it got out to the network, even if we didn't get the blink quorum reply
|
|
// for some reason.
|
|
auto status = future.wait_for(10s);
|
|
if (status != std::future_status::ready) {
|
|
tx.response["status"] = STATUS_FAILED;
|
|
tx.response["reason"] = "Blink quorum timeout";
|
|
tx.response["blink_status"] = blink_result::timeout;
|
|
return;
|
|
}
|
|
|
|
try {
|
|
auto result = future.get();
|
|
tx.response["blink_status"] = result.first;
|
|
if (result.first == blink_result::accepted) {
|
|
tx.response["status"] = STATUS_OK;
|
|
} else {
|
|
tx.response["status"] = STATUS_FAILED;
|
|
tx.response["reason"] = !result.second.empty() ? result.second : result.first == blink_result::timeout ? "Blink quorum timeout" : "Transaction rejected by blink quorum";
|
|
}
|
|
} catch (const std::exception &e) {
|
|
tx.response["blink_status"] = blink_result::rejected;
|
|
tx.response["status"] = STATUS_FAILED;
|
|
tx.response["reason"] = "Transaction failed: "s + e.what();
|
|
}
|
|
return;
|
|
}
|
|
|
|
tx_verification_context tvc{};
|
|
if (!m_core.handle_incoming_tx(tx.request.tx, tvc, tx_pool_options::new_tx()) || tvc.m_verifivation_failed || !tvc.m_should_be_relayed)
|
|
{
|
|
tx.response["status"] = STATUS_FAILED;
|
|
auto reason = print_tx_verification_context(tvc);
|
|
log::warning(logcat, "[on_send_raw_tx]: {} {}", (tvc.m_verifivation_failed ? "tx verification failed" : "Failed to process tx"), reason);
|
|
tx.response["reason"] = std::move(reason);
|
|
tx.response["reason_codes"] = tx_verification_failure_codes(tvc);
|
|
return;
|
|
}
|
|
|
|
// Why is is the RPC handler's responsibility to tell the p2p protocol to relay a transaction?!
|
|
NOTIFY_NEW_TRANSACTIONS::request r{};
|
|
r.txs.push_back(std::move(tx.request.tx));
|
|
cryptonote_connection_context fake_context{};
|
|
m_core.get_protocol()->relay_transactions(r, fake_context);
|
|
|
|
tx.response["status"] = STATUS_OK;
|
|
return;
|
|
}
|
|
//------------------------------------------------------------------------------------------------------------------------------
|
|
void core_rpc_server::invoke(START_MINING& start_mining, rpc_context context)
|
|
{
|
|
//CHECK_CORE_READY();
|
|
if(!check_core_ready()){
|
|
start_mining.response["status"] = STATUS_BUSY;
|
|
return;
|
|
}
|
|
|
|
cryptonote::address_parse_info info;
|
|
if(!get_account_address_from_str(info, m_core.get_nettype(), start_mining.request.miner_address)) {
|
|
start_mining.response["status"] = "Failed, invalid address";
|
|
log::warning(logcat, start_mining.response["status"]);
|
|
return;
|
|
}
|
|
if (info.is_subaddress) {
|
|
start_mining.response["status"] = "Mining to subaddress isn't supported yet";
|
|
log::warning(logcat, start_mining.response["status"]);
|
|
return;
|
|
}
|
|
|
|
int max_concurrency_count = std::thread::hardware_concurrency() * 4;
|
|
|
|
// if we couldn't detect threads, set it to a ridiculously high number
|
|
if (max_concurrency_count == 0)
|
|
max_concurrency_count = 257;
|
|
|
|
// if there are more threads requested than the hardware supports
|
|
// then we fail and log that.
|
|
if (start_mining.request.threads_count > max_concurrency_count) {
|
|
start_mining.response["status"] = "Failed, too many threads relative to CPU cores.";
|
|
log::warning(logcat, start_mining.response["status"]);
|
|
return;
|
|
}
|
|
|
|
auto& miner = m_core.get_miner();
|
|
if (miner.is_mining())
|
|
{
|
|
start_mining.response["status"] = "Already mining";
|
|
return;
|
|
}
|
|
|
|
if(!miner.start(info.address, start_mining.request.threads_count, start_mining.request.num_blocks, start_mining.request.slow_mining))
|
|
{
|
|
start_mining.response["status"] = "Failed, mining not started";
|
|
log::warning(logcat, start_mining.response["status"]);
|
|
return;
|
|
}
|
|
|
|
start_mining.response["status"] = STATUS_OK;
|
|
}
|
|
//------------------------------------------------------------------------------------------------------------------------------
|
|
void core_rpc_server::invoke(STOP_MINING& stop_mining, rpc_context context)
|
|
{
|
|
cryptonote::miner &miner= m_core.get_miner();
|
|
if(!miner.is_mining())
|
|
{
|
|
stop_mining.response["status"] = "Mining never started";
|
|
log::warning(logcat, stop_mining.response["status"]);
|
|
return;
|
|
}
|
|
if(!miner.stop())
|
|
{
|
|
stop_mining.response["status"] = "Failed, mining not stopped";
|
|
log::warning(logcat, stop_mining.response["status"]);
|
|
return;
|
|
}
|
|
|
|
stop_mining.response["status"] = STATUS_OK;
|
|
}
|
|
//------------------------------------------------------------------------------------------------------------------------------
|
|
void core_rpc_server::invoke(MINING_STATUS& mining_status, rpc_context context)
|
|
{
|
|
const miner& lMiner = m_core.get_miner();
|
|
mining_status.response["active"] = lMiner.is_mining();
|
|
mining_status.response["block_target"] = tools::to_seconds(TARGET_BLOCK_TIME);
|
|
mining_status.response["difficulty"] = m_core.get_blockchain_storage().get_difficulty_for_next_block(false /*pulse*/);
|
|
if ( lMiner.is_mining() ) {
|
|
mining_status.response["speed"] = std::lround(lMiner.get_speed());
|
|
mining_status.response["threads_count"] = lMiner.get_threads_count();
|
|
mining_status.response["block_reward"] = lMiner.get_block_reward();
|
|
}
|
|
const account_public_address& lMiningAdr = lMiner.get_mining_address();
|
|
if (lMiner.is_mining())
|
|
mining_status.response["address"] = get_account_address_as_str(nettype(), false, lMiningAdr);
|
|
const auto major_version = m_core.get_blockchain_storage().get_network_version();
|
|
|
|
mining_status.response["pow_algorithm"] =
|
|
major_version >= hf::hf12_checkpointing ? "RandomX (OXEN variant)" :
|
|
major_version == hf::hf11_infinite_staking ? "Cryptonight Turtle Light (Variant 2)" :
|
|
"Cryptonight Heavy (Variant 2)";
|
|
|
|
mining_status.response["status"] = STATUS_OK;
|
|
}
|
|
//------------------------------------------------------------------------------------------------------------------------------
|
|
void core_rpc_server::invoke(SAVE_BC& save_bc, rpc_context context)
|
|
{
|
|
if( !m_core.get_blockchain_storage().store_blockchain() )
|
|
{
|
|
save_bc.response["status"] = "Error while storing blockchain";
|
|
log::warning(logcat, save_bc.response["status"]);
|
|
return;
|
|
}
|
|
save_bc.response["status"] = STATUS_OK;
|
|
}
|
|
|
|
static nlohmann::json json_peer_info(const nodetool::peerlist_entry& peer) {
|
|
auto addr_type = peer.adr.get_type_id();
|
|
nlohmann::json p{
|
|
{"id", peer.id},
|
|
{"host", peer.adr.host_str()},
|
|
{"port", peer.adr.port()},
|
|
{"last_seen", peer.last_seen}
|
|
};
|
|
if (peer.pruning_seed) p["pruning_seed"] = peer.pruning_seed;
|
|
return p;
|
|
}
|
|
|
|
//------------------------------------------------------------------------------------------------------------------------------
|
|
void core_rpc_server::invoke(GET_PEER_LIST& pl, rpc_context context)
|
|
{
|
|
std::vector<nodetool::peerlist_entry> white_list, gray_list;
|
|
if (pl.request.public_only)
|
|
m_p2p.get_public_peerlist(gray_list, white_list);
|
|
else
|
|
m_p2p.get_peerlist(gray_list, white_list);
|
|
|
|
std::transform(white_list.begin(), white_list.end(), std::back_inserter(pl.response["white_list"]), json_peer_info);
|
|
std::transform(gray_list.begin(), gray_list.end(), std::back_inserter(pl.response["gray_list"]), json_peer_info);
|
|
|
|
pl.response["status"] = STATUS_OK;
|
|
}
|
|
//------------------------------------------------------------------------------------------------------------------------------
|
|
void core_rpc_server::invoke(SET_LOG_LEVEL& set_log_level, rpc_context context)
|
|
{
|
|
if (set_log_level.request.level < 0 || set_log_level.request.level > 4)
|
|
{
|
|
set_log_level.response["status"] = "Error: log level not valid";
|
|
return;
|
|
}
|
|
auto log_level = oxen::logging::parse_level(set_log_level.request.level);
|
|
if (log_level.has_value())
|
|
log::reset_level(*log_level);
|
|
set_log_level.response["status"] = STATUS_OK;
|
|
}
|
|
//------------------------------------------------------------------------------------------------------------------------------
|
|
void core_rpc_server::invoke(SET_LOG_CATEGORIES& set_log_categories, rpc_context context)
|
|
{
|
|
oxen::logging::process_categories_string(set_log_categories.request.categories.c_str());
|
|
set_log_categories.response["status"] = STATUS_OK;
|
|
}
|
|
//------------------------------------------------------------------------------------------------------------------------------
|
|
GET_TRANSACTION_POOL_HASHES_BIN::response core_rpc_server::invoke(GET_TRANSACTION_POOL_HASHES_BIN::request&& req, rpc_context context)
|
|
{
|
|
GET_TRANSACTION_POOL_HASHES_BIN::response res{};
|
|
|
|
std::vector<crypto::hash> tx_pool_hashes;
|
|
m_core.get_pool().get_transaction_hashes(tx_pool_hashes, context.admin, req.blinked_txs_only);
|
|
|
|
res.tx_hashes = std::move(tx_pool_hashes);
|
|
res.status = STATUS_OK;
|
|
return res;
|
|
}
|
|
//------------------------------------------------------------------------------------------------------------------------------
|
|
void core_rpc_server::invoke(GET_TRANSACTION_POOL_HASHES& get_transaction_pool_hashes, rpc_context context)
|
|
{
|
|
std::vector<crypto::hash> tx_hashes;
|
|
m_core.get_pool().get_transaction_hashes(tx_hashes, context.admin);
|
|
get_transaction_pool_hashes.response_hex["tx_hashes"] = tx_hashes;
|
|
get_transaction_pool_hashes.response["status"] = STATUS_OK;
|
|
}
|
|
//------------------------------------------------------------------------------------------------------------------------------
|
|
void core_rpc_server::invoke(GET_TRANSACTION_POOL_STATS& stats, rpc_context context)
|
|
{
|
|
auto txpool = m_core.get_pool().get_transaction_stats(stats.request.include_unrelayed);
|
|
json pool_stats{
|
|
{"bytes_total", txpool.bytes_total},
|
|
{"bytes_min", txpool.bytes_min},
|
|
{"bytes_max", txpool.bytes_max},
|
|
{"bytes_med", txpool.bytes_med},
|
|
{"fee_total", txpool.fee_total},
|
|
{"oldest", txpool.oldest},
|
|
{"txs_total", txpool.txs_total},
|
|
{"num_failing", txpool.num_failing},
|
|
{"num_10m", txpool.num_10m},
|
|
{"num_not_relayed", txpool.num_not_relayed},
|
|
{"histo", std::move(txpool.histo)},
|
|
{"num_double_spends", txpool.num_double_spends}};
|
|
|
|
if (txpool.histo_98pc)
|
|
pool_stats["histo_98pc"] = txpool.histo_98pc;
|
|
else
|
|
pool_stats["histo_max"] = std::time(nullptr) - txpool.oldest;
|
|
|
|
stats.response["pool_stats"] = std::move(pool_stats);
|
|
stats.response["status"] = STATUS_OK;
|
|
}
|
|
//------------------------------------------------------------------------------------------------------------------------------
|
|
void core_rpc_server::invoke(STOP_DAEMON& stop_daemon, rpc_context context)
|
|
{
|
|
m_p2p.send_stop_signal();
|
|
stop_daemon.response["status"] = STATUS_OK;
|
|
}
|
|
//------------------------------------------------------------------------------------------------------------------------------
|
|
//
|
|
// Oxen
|
|
//
|
|
GET_OUTPUT_BLACKLIST_BIN::response core_rpc_server::invoke(GET_OUTPUT_BLACKLIST_BIN::request&& req, rpc_context context)
|
|
{
|
|
GET_OUTPUT_BLACKLIST_BIN::response res{};
|
|
|
|
try
|
|
{
|
|
m_core.get_output_blacklist(res.blacklist);
|
|
}
|
|
catch (const std::exception &e)
|
|
{
|
|
res.status = std::string("Failed to get output blacklist: ") + e.what();
|
|
return res;
|
|
}
|
|
|
|
res.status = STATUS_OK;
|
|
return res;
|
|
}
|
|
//------------------------------------------------------------------------------------------------------------------------------
|
|
void core_rpc_server::invoke(GET_BLOCK_COUNT& get, rpc_context context)
|
|
{
|
|
get.response["count"] = m_core.get_current_blockchain_height();
|
|
get.response["status"] = STATUS_OK;
|
|
}
|
|
//------------------------------------------------------------------------------------------------------------------------------
|
|
void core_rpc_server::invoke(GET_BLOCK_HASH& get, rpc_context context)
|
|
{
|
|
auto curr_height = m_core.get_current_blockchain_height();
|
|
for (auto h : get.request.heights) {
|
|
if (h >= curr_height)
|
|
throw rpc_error{ERROR_TOO_BIG_HEIGHT,
|
|
"Requested block height {} greater than current top block height {}"_format(
|
|
h, curr_height - 1)};
|
|
get.response_hex["{}"_format(h)] = m_core.get_block_id_by_height(h);
|
|
}
|
|
get.response["height"] = curr_height;
|
|
get.response["status"] = STATUS_OK;
|
|
}
|
|
//------------------------------------------------------------------------------------------------------------------------------
|
|
uint64_t core_rpc_server::get_block_reward(const block& blk)
|
|
{
|
|
uint64_t reward = 0;
|
|
for(const tx_out& out: blk.miner_tx.vout)
|
|
{
|
|
reward += out.amount;
|
|
}
|
|
return reward;
|
|
}
|
|
//------------------------------------------------------------------------------------------------------------------------------
|
|
void core_rpc_server::fill_block_header_response(
|
|
const block& blk,
|
|
bool orphan_status,
|
|
uint64_t height,
|
|
const crypto::hash& hash,
|
|
block_header_response& response,
|
|
bool fill_pow_hash,
|
|
bool get_tx_hashes)
|
|
{
|
|
response.major_version = static_cast<uint8_t>(blk.major_version);
|
|
response.minor_version = blk.minor_version;
|
|
response.timestamp = blk.timestamp;
|
|
response.prev_hash = tools::type_to_hex(blk.prev_id);
|
|
response.nonce = blk.nonce;
|
|
response.orphan_status = orphan_status;
|
|
response.height = height;
|
|
response.depth = m_core.get_current_blockchain_height() - height - 1;
|
|
response.hash = tools::type_to_hex(hash);
|
|
response.difficulty = m_core.get_blockchain_storage().block_difficulty(height);
|
|
response.cumulative_difficulty = m_core.get_blockchain_storage().get_db().get_block_cumulative_difficulty(height);
|
|
response.block_weight = m_core.get_blockchain_storage().get_db().get_block_weight(height);
|
|
response.reward = (blk.reward > 0) ? blk.reward : get_block_reward(blk);
|
|
response.block_size = response.block_weight = m_core.get_blockchain_storage().get_db().get_block_weight(height);
|
|
response.num_txes = blk.tx_hashes.size();
|
|
if (fill_pow_hash)
|
|
response.pow_hash = tools::type_to_hex(
|
|
get_block_longhash_w_blockchain(
|
|
m_core.get_nettype(),
|
|
&m_core.get_blockchain_storage(),
|
|
blk,
|
|
height,
|
|
0));
|
|
response.long_term_weight = m_core.get_blockchain_storage().get_db().get_block_long_term_weight(height);
|
|
response.service_node_winner =
|
|
tools::type_to_hex(blk.service_node_winner_key) == ""
|
|
? tools::type_to_hex(cryptonote::get_service_node_winner_from_tx_extra(blk.miner_tx.extra))
|
|
: tools::type_to_hex(blk.service_node_winner_key);
|
|
response.coinbase_payouts = get_block_reward(blk);
|
|
if (blk.miner_tx.vout.size() > 0)
|
|
response.miner_tx_hash = tools::type_to_hex(cryptonote::get_transaction_hash(blk.miner_tx));
|
|
if (get_tx_hashes)
|
|
{
|
|
response.tx_hashes.reserve(blk.tx_hashes.size());
|
|
for (const auto& tx_hash : blk.tx_hashes)
|
|
response.tx_hashes.push_back(tools::type_to_hex(tx_hash));
|
|
}
|
|
}
|
|
//------------------------------------------------------------------------------------------------------------------------------
|
|
void core_rpc_server::invoke(GET_LAST_BLOCK_HEADER& get_last_block_header, rpc_context context)
|
|
{
|
|
if(!check_core_ready())
|
|
{
|
|
get_last_block_header.response["status"] = STATUS_BUSY;
|
|
return;
|
|
}
|
|
|
|
auto [last_block_height, last_block_hash] = m_core.get_blockchain_top();
|
|
block last_block;
|
|
bool have_last_block = m_core.get_block_by_height(last_block_height, last_block);
|
|
if (!have_last_block)
|
|
throw rpc_error{ERROR_INTERNAL, "Internal error: can't get last block."};
|
|
block_header_response header{};
|
|
fill_block_header_response(last_block, false, last_block_height, last_block_hash, header, get_last_block_header.request.fill_pow_hash && context.admin, get_last_block_header.request.get_tx_hashes);
|
|
|
|
nlohmann::json header_as_json = header;
|
|
get_last_block_header.response["block_header"] = header_as_json;
|
|
get_last_block_header.response["status"] = STATUS_OK;
|
|
return;
|
|
}
|
|
//------------------------------------------------------------------------------------------------------------------------------
|
|
|
|
void core_rpc_server::invoke(GET_BLOCK_HEADER_BY_HASH& get_block_header_by_hash, rpc_context context)
|
|
{
|
|
auto get = [this, &get_block_header_by_hash, admin=context.admin](const std::string &hash, block_header_response &block_header) {
|
|
crypto::hash block_hash;
|
|
if (!tools::hex_to_type(hash, block_hash))
|
|
throw rpc_error{ERROR_WRONG_PARAM, "Failed to parse hex representation of block hash. Hex = " + hash + '.'};
|
|
block blk;
|
|
bool orphan = false;
|
|
bool have_block = m_core.get_block_by_hash(block_hash, blk, &orphan);
|
|
if (!have_block)
|
|
throw rpc_error{ERROR_INTERNAL, "Internal error: can't get block by hash. Hash = " + hash + '.'};
|
|
if (blk.miner_tx.vin.size() != 1 || !std::holds_alternative<txin_gen>(blk.miner_tx.vin.front()))
|
|
throw rpc_error{ERROR_INTERNAL, "Internal error: coinbase transaction in the block has the wrong type"};
|
|
uint64_t block_height = var::get<txin_gen>(blk.miner_tx.vin.front()).height;
|
|
fill_block_header_response(blk, orphan, block_height, block_hash, block_header, get_block_header_by_hash.request.fill_pow_hash && admin, get_block_header_by_hash.request.get_tx_hashes);
|
|
};
|
|
|
|
if (!get_block_header_by_hash.request.hash.empty())
|
|
{
|
|
block_header_response block_header;
|
|
get(get_block_header_by_hash.request.hash, block_header);
|
|
get_block_header_by_hash.response["block_header"] = block_header;
|
|
}
|
|
|
|
std::vector<block_header_response> block_headers;
|
|
for (const std::string &hash: get_block_header_by_hash.request.hashes)
|
|
get(hash, block_headers.emplace_back());
|
|
|
|
get_block_header_by_hash.response["block_headers"] = block_headers;
|
|
get_block_header_by_hash.response["status"] = STATUS_OK;
|
|
return;
|
|
}
|
|
//------------------------------------------------------------------------------------------------------------------------------
|
|
void core_rpc_server::invoke(GET_BLOCK_HEADERS_RANGE& get_block_headers_range, rpc_context context)
|
|
{
|
|
const uint64_t bc_height = m_core.get_current_blockchain_height();
|
|
uint64_t start_height = get_block_headers_range.request.start_height;
|
|
uint64_t end_height = get_block_headers_range.request.end_height;
|
|
if (start_height >= bc_height || end_height >= bc_height || start_height > end_height)
|
|
throw rpc_error{ERROR_TOO_BIG_HEIGHT, "Invalid start/end heights."};
|
|
std::vector<block_header_response> headers;
|
|
for (uint64_t h = start_height; h <= end_height; ++h)
|
|
{
|
|
block blk;
|
|
bool have_block = m_core.get_block_by_height(h, blk);
|
|
if (!have_block)
|
|
throw rpc_error{ERROR_INTERNAL,
|
|
"Internal error: can't get block by height. Height = {}."_format(h)};
|
|
if (blk.miner_tx.vin.size() != 1 || !std::holds_alternative<txin_gen>(blk.miner_tx.vin.front()))
|
|
throw rpc_error{ERROR_INTERNAL, "Internal error: coinbase transaction in the block has the wrong type"};
|
|
uint64_t block_height = var::get<txin_gen>(blk.miner_tx.vin.front()).height;
|
|
if (block_height != h)
|
|
throw rpc_error{ERROR_INTERNAL, "Internal error: coinbase transaction in the block has the wrong height"};
|
|
auto& hdr = headers.emplace_back();
|
|
fill_block_header_response(blk, false, block_height, get_block_hash(blk), hdr, get_block_headers_range.request.fill_pow_hash && context.admin, get_block_headers_range.request.get_tx_hashes);
|
|
}
|
|
get_block_headers_range.response["headers"] = headers;
|
|
get_block_headers_range.response["status"] = STATUS_OK;
|
|
return;
|
|
}
|
|
//------------------------------------------------------------------------------------------------------------------------------
|
|
void core_rpc_server::invoke(GET_BLOCK_HEADER_BY_HEIGHT& get_block_header_by_height, rpc_context context)
|
|
{
|
|
auto get = [this, curr_height=m_core.get_current_blockchain_height(), pow=get_block_header_by_height.request.fill_pow_hash && context.admin, tx_hashes=get_block_header_by_height.request.get_tx_hashes]
|
|
(uint64_t height, block_header_response& bhr) {
|
|
if (height >= curr_height)
|
|
throw rpc_error{ERROR_TOO_BIG_HEIGHT,
|
|
"Requested block height: " + std::to_string(height) + " greater than current top block height: " + std::to_string(curr_height - 1)};
|
|
block blk;
|
|
bool have_block = m_core.get_block_by_height(height, blk);
|
|
if (!have_block)
|
|
throw rpc_error{ERROR_INTERNAL, "Internal error: can't get block by height. Height = " + std::to_string(height) + '.'};
|
|
fill_block_header_response(blk, false, height, get_block_hash(blk), bhr, pow, tx_hashes);
|
|
};
|
|
|
|
|
|
block_header_response header;
|
|
if (get_block_header_by_height.request.height)
|
|
{
|
|
get(*get_block_header_by_height.request.height, header);
|
|
get_block_header_by_height.response["block_header"] = header;
|
|
}
|
|
std::vector<block_header_response> headers;
|
|
if (!get_block_header_by_height.request.heights.empty())
|
|
headers.reserve(get_block_header_by_height.request.heights.size());
|
|
for (auto height : get_block_header_by_height.request.heights)
|
|
get(height, headers.emplace_back());
|
|
|
|
get_block_header_by_height.response["status"] = STATUS_OK;
|
|
get_block_header_by_height.response["block_headers"] = headers;
|
|
return;
|
|
}
|
|
//------------------------------------------------------------------------------------------------------------------------------
|
|
void core_rpc_server::invoke(GET_BLOCK& get_block, rpc_context context)
|
|
{
|
|
block blk;
|
|
uint64_t block_height;
|
|
bool orphan = false;
|
|
crypto::hash block_hash;
|
|
if (!get_block.request.hash.empty())
|
|
{
|
|
if (!tools::hex_to_type(get_block.request.hash, block_hash))
|
|
throw rpc_error{ERROR_WRONG_PARAM, "Failed to parse hex representation of block hash. Hex = " + get_block.request.hash + '.'};
|
|
if (!m_core.get_block_by_hash(block_hash, blk, &orphan))
|
|
throw rpc_error{ERROR_INTERNAL, "Internal error: can't get block by hash. Hash = " + get_block.request.hash + '.'};
|
|
if (blk.miner_tx.vin.size() != 1 || !std::holds_alternative<txin_gen>(blk.miner_tx.vin.front()))
|
|
throw rpc_error{ERROR_INTERNAL, "Internal error: coinbase transaction in the block has the wrong type"};
|
|
block_height = var::get<txin_gen>(blk.miner_tx.vin.front()).height;
|
|
}
|
|
else
|
|
{
|
|
if (auto curr_height = m_core.get_current_blockchain_height(); get_block.request.height >= curr_height)
|
|
throw rpc_error{ERROR_TOO_BIG_HEIGHT, std::string("Requested block height: ") + std::to_string(get_block.request.height) + " greater than current top block height: " + std::to_string(curr_height - 1)};
|
|
if (!m_core.get_block_by_height(get_block.request.height, blk))
|
|
throw rpc_error{ERROR_INTERNAL, "Internal error: can't get block by height. Height = " + std::to_string(get_block.request.height) + '.'};
|
|
block_hash = get_block_hash(blk);
|
|
block_height = get_block.request.height;
|
|
}
|
|
block_header_response header;
|
|
fill_block_header_response(blk, orphan, block_height, block_hash, header, get_block.request.fill_pow_hash && context.admin, false /*tx hashes*/);
|
|
get_block.response["block_header"] = header;
|
|
std::vector<std::string> tx_hashes;
|
|
tx_hashes.reserve(blk.tx_hashes.size());
|
|
std::transform(blk.tx_hashes.begin(), blk.tx_hashes.end(), std::back_inserter(tx_hashes), [](const auto& x) { return tools::type_to_hex(x); });
|
|
get_block.response["tx_hashes"] = tx_hashes;
|
|
get_block.response["blob"] = oxenc::to_hex(t_serializable_object_to_blob(blk));
|
|
get_block.response["json"] = obj_to_json_str(blk);
|
|
get_block.response["status"] = STATUS_OK;
|
|
return;
|
|
}
|
|
|
|
static json json_connection_info(const connection_info& ci) {
|
|
json info{
|
|
{"incoming", ci.incoming},
|
|
{"ip", ci.ip},
|
|
{"address_type", ci.address_type},
|
|
{"peer_id", ci.peer_id},
|
|
{"recv_count", ci.recv_count},
|
|
{"recv_idle_ms", ci.recv_idle_time.count()},
|
|
{"send_count", ci.send_count},
|
|
{"send_idle_ms", ci.send_idle_time.count()},
|
|
{"state", ci.state},
|
|
{"live_ms", ci.live_time.count()},
|
|
{"avg_download", ci.avg_download},
|
|
{"current_download", ci.current_download},
|
|
{"avg_upload", ci.avg_upload},
|
|
{"current_upload", ci.current_upload},
|
|
{"connection_id", ci.connection_id},
|
|
{"height", ci.height},
|
|
};
|
|
if (ci.ip != ci.host) info["host"] = ci.host;
|
|
if (ci.localhost) info["localhost"] = true;
|
|
if (ci.local_ip) info["local_ip"] = true;
|
|
if (uint16_t port; tools::parse_int(ci.port, port) && port > 0) info["port"] = port;
|
|
// Included for completeness, but undocumented as this is not currently actually used or
|
|
// supported on Oxen:
|
|
if (ci.pruning_seed) info["pruning_seed"] = ci.pruning_seed;
|
|
return info;
|
|
}
|
|
|
|
//------------------------------------------------------------------------------------------------------------------------------
|
|
void core_rpc_server::invoke(GET_CONNECTIONS& get_connections, rpc_context context)
|
|
{
|
|
auto& c = get_connections.response["connections"];
|
|
c = json::array();
|
|
for (auto& ci : m_p2p.get_payload_object().get_connections())
|
|
c.push_back(json_connection_info(ci));
|
|
get_connections.response["status"] = STATUS_OK;
|
|
}
|
|
//------------------------------------------------------------------------------------------------------------------------------
|
|
void core_rpc_server::invoke(HARD_FORK_INFO& hfinfo, rpc_context context)
|
|
{
|
|
const auto& blockchain = m_core.get_blockchain_storage();
|
|
auto version =
|
|
hfinfo.request.version > 0 ? static_cast<hf>(hfinfo.request.version) :
|
|
hfinfo.request.height > 0 ? blockchain.get_network_version(hfinfo.request.height) :
|
|
blockchain.get_network_version();
|
|
hfinfo.response["version"] = version;
|
|
hfinfo.response["enabled"] = blockchain.get_network_version() >= version;
|
|
auto heights = get_hard_fork_heights(m_core.get_nettype(), version);
|
|
if (heights.first)
|
|
hfinfo.response["earliest_height"] = *heights.first;
|
|
if (heights.second)
|
|
hfinfo.response["latest_height"] = *heights.second;
|
|
hfinfo.response["status"] = STATUS_OK;
|
|
}
|
|
//------------------------------------------------------------------------------------------------------------------------------
|
|
void core_rpc_server::invoke(GET_BANS& get_bans, rpc_context context)
|
|
{
|
|
auto now = time(nullptr);
|
|
std::map<std::string, time_t> blocked_hosts = m_p2p.get_blocked_hosts();
|
|
for (std::map<std::string, time_t>::const_iterator i = blocked_hosts.begin(); i != blocked_hosts.end(); ++i)
|
|
{
|
|
if (i->second > now) {
|
|
ban b;
|
|
b.host = i->first;
|
|
b.seconds = i->second - now;
|
|
get_bans.response["bans"].push_back(b);
|
|
}
|
|
}
|
|
std::map<epee::net_utils::ipv4_network_subnet, time_t> blocked_subnets = m_p2p.get_blocked_subnets();
|
|
for (std::map<epee::net_utils::ipv4_network_subnet, time_t>::const_iterator i = blocked_subnets.begin(); i != blocked_subnets.end(); ++i)
|
|
{
|
|
if (i->second > now) {
|
|
ban b;
|
|
b.host = i->first.host_str();
|
|
b.seconds = i->second - now;
|
|
get_bans.response["bans"].push_back(b);
|
|
}
|
|
}
|
|
|
|
get_bans.response["status"] = STATUS_OK;
|
|
return;
|
|
}
|
|
//------------------------------------------------------------------------------------------------------------------------------
|
|
void core_rpc_server::invoke(BANNED& banned, rpc_context context)
|
|
{
|
|
auto na_parsed = net::get_network_address(banned.request.address, 0);
|
|
if (!na_parsed)
|
|
throw rpc_error{ERROR_WRONG_PARAM, "Unsupported host type"};
|
|
epee::net_utils::network_address na = std::move(*na_parsed);
|
|
|
|
time_t seconds;
|
|
if (m_p2p.is_host_blocked(na, &seconds))
|
|
{
|
|
banned.response["banned"] = true;
|
|
banned.response["seconds"] = seconds;
|
|
}
|
|
else
|
|
{
|
|
banned.response["banned"] = false;
|
|
banned.response["seconds"] = 0;
|
|
}
|
|
|
|
banned.response["status"] = STATUS_OK;
|
|
}
|
|
//------------------------------------------------------------------------------------------------------------------------------
|
|
void core_rpc_server::invoke(SET_BANS& set_bans, rpc_context context)
|
|
{
|
|
epee::net_utils::network_address na;
|
|
// try subnet first
|
|
auto ns_parsed = net::get_ipv4_subnet_address(set_bans.request.host);
|
|
if (ns_parsed)
|
|
{
|
|
if (set_bans.request.ban)
|
|
m_p2p.block_subnet(*ns_parsed, set_bans.request.seconds);
|
|
else
|
|
m_p2p.unblock_subnet(*ns_parsed);
|
|
set_bans.response["status"] = STATUS_OK;
|
|
return;
|
|
}
|
|
|
|
// then host
|
|
auto na_parsed = net::get_network_address(set_bans.request.host, 0);
|
|
if (!na_parsed)
|
|
throw rpc_error{ERROR_WRONG_PARAM, "Unsupported host/subnet type"};
|
|
na = std::move(*na_parsed);
|
|
if (set_bans.request.ban)
|
|
m_p2p.block_host(na, set_bans.request.seconds);
|
|
else
|
|
m_p2p.unblock_host(na);
|
|
|
|
set_bans.response["status"] = STATUS_OK;
|
|
return;
|
|
}
|
|
//------------------------------------------------------------------------------------------------------------------------------
|
|
void core_rpc_server::invoke(FLUSH_TRANSACTION_POOL& flush_transaction_pool, rpc_context context)
|
|
{
|
|
bool failed = false;
|
|
std::vector<crypto::hash> txids;
|
|
if (flush_transaction_pool.request.txids.empty())
|
|
{
|
|
std::vector<transaction> pool_txs;
|
|
m_core.get_pool().get_transactions(pool_txs);
|
|
for (const auto &tx: pool_txs)
|
|
{
|
|
txids.push_back(cryptonote::get_transaction_hash(tx));
|
|
}
|
|
}
|
|
else
|
|
{
|
|
for (const auto &txid_hex: flush_transaction_pool.request.txids)
|
|
{
|
|
std::string txid_data;
|
|
if (!tools::hex_to_type(txid_hex, txids.emplace_back()))
|
|
{
|
|
failed = true;
|
|
txids.pop_back();
|
|
}
|
|
}
|
|
}
|
|
if (!m_core.get_blockchain_storage().flush_txes_from_pool(txids))
|
|
{
|
|
flush_transaction_pool.response["status"] = "Failed to remove one or more tx(es)";
|
|
return;
|
|
}
|
|
|
|
flush_transaction_pool.response["status"] = failed
|
|
? txids.empty()
|
|
? "Failed to parse txid"
|
|
: "Failed to parse some of the txids"
|
|
: STATUS_OK;
|
|
}
|
|
//------------------------------------------------------------------------------------------------------------------------------
|
|
void core_rpc_server::invoke(GET_OUTPUT_HISTOGRAM& get_output_histogram, rpc_context context)
|
|
{
|
|
if (!context.admin && get_output_histogram.request.recent_cutoff > 0 && get_output_histogram.request.recent_cutoff < (uint64_t)time(NULL) - OUTPUT_HISTOGRAM_RECENT_CUTOFF_RESTRICTION)
|
|
{
|
|
get_output_histogram.response["status"] = "Recent cutoff is too old";
|
|
return;
|
|
}
|
|
|
|
std::map<uint64_t, std::tuple<uint64_t, uint64_t, uint64_t>> histogram;
|
|
try
|
|
{
|
|
histogram = m_core.get_blockchain_storage().get_output_histogram(
|
|
get_output_histogram.request.amounts,
|
|
get_output_histogram.request.unlocked,
|
|
get_output_histogram.request.recent_cutoff,
|
|
get_output_histogram.request.min_count
|
|
);
|
|
}
|
|
catch (const std::exception &e)
|
|
{
|
|
get_output_histogram.response["status"] = "Failed to get output histogram";
|
|
return;
|
|
}
|
|
|
|
std::vector<GET_OUTPUT_HISTOGRAM::entry> response_histogram;
|
|
response_histogram.reserve(histogram.size());
|
|
for (const auto &[amount, histogram_tuple]: histogram)
|
|
{
|
|
auto& [total_instances, unlocked_instances, recent_instances] = histogram_tuple;
|
|
|
|
if (total_instances >= get_output_histogram.request.min_count && (total_instances <= get_output_histogram.request.max_count || get_output_histogram.request.max_count == 0))
|
|
response_histogram.push_back(GET_OUTPUT_HISTOGRAM::entry{amount, total_instances, unlocked_instances, recent_instances});
|
|
}
|
|
|
|
get_output_histogram.response["histogram"] = response_histogram;
|
|
get_output_histogram.response["status"] = STATUS_OK;
|
|
return;
|
|
}
|
|
//------------------------------------------------------------------------------------------------------------------------------
|
|
void core_rpc_server::invoke(GET_VERSION& version, rpc_context context)
|
|
{
|
|
version.response["version"] = pack_version(VERSION);
|
|
version.response["status"] = STATUS_OK;
|
|
}
|
|
//------------------------------------------------------------------------------------------------------------------------------
|
|
void core_rpc_server::invoke(GET_SERVICE_NODE_STATUS& sns, rpc_context context)
|
|
{
|
|
auto [top_height, top_hash] = m_core.get_blockchain_top();
|
|
sns.response["height"] = top_height;
|
|
sns.response_hex["block_hash"] = top_hash;
|
|
const auto& keys = m_core.get_service_keys();
|
|
if (!keys.pub) {
|
|
sns.response["status"] = "Not a service node";
|
|
return;
|
|
}
|
|
sns.response["status"] = STATUS_OK;
|
|
|
|
auto sn_infos = m_core.get_service_node_list_state({{keys.pub}});
|
|
if (!sn_infos.empty())
|
|
fill_sn_response_entry(sns.response["service_node_state"] = json::object(), sns.is_bt(), {} /*all fields*/, sn_infos.front(), top_height);
|
|
else {
|
|
sns.response["service_node_state"] = json{
|
|
{"public_ip", epee::string_tools::get_ip_string_from_int32(m_core.sn_public_ip())},
|
|
{"storage_port", m_core.storage_https_port()},
|
|
{"storage_lmq_port", m_core.storage_omq_port()},
|
|
{"quorumnet_port", m_core.quorumnet_port()},
|
|
{"service_node_version", OXEN_VERSION}
|
|
};
|
|
auto rhex = sns.response_hex["service_node_state"];
|
|
rhex["service_node_pubkey"] = keys.pub;
|
|
rhex["pubkey_ed25519"] = keys.pub_ed25519;
|
|
rhex["pubkey_x25519"] = keys.pub_x25519;
|
|
}
|
|
}
|
|
|
|
|
|
//------------------------------------------------------------------------------------------------------------------------------
|
|
void core_rpc_server::invoke(GET_COINBASE_TX_SUM& get_coinbase_tx_sum, rpc_context context)
|
|
{
|
|
if (auto sums = m_core.get_coinbase_tx_sum(get_coinbase_tx_sum.request.height, get_coinbase_tx_sum.request.count)) {
|
|
std::tie(get_coinbase_tx_sum.response["emission_amount"], get_coinbase_tx_sum.response["fee_amount"], get_coinbase_tx_sum.response["burn_amount"]) = *sums;
|
|
get_coinbase_tx_sum.response["status"] = STATUS_OK;
|
|
} else {
|
|
get_coinbase_tx_sum.response["status"] = STATUS_BUSY; // some other request is already calculating it
|
|
}
|
|
}
|
|
//------------------------------------------------------------------------------------------------------------------------------
|
|
void core_rpc_server::invoke(GET_BASE_FEE_ESTIMATE& get_base_fee_estimate, rpc_context context)
|
|
{
|
|
auto fees = m_core.get_blockchain_storage().get_dynamic_base_fee_estimate(get_base_fee_estimate.request.grace_blocks);
|
|
get_base_fee_estimate.response["fee_per_byte"] = fees.first;
|
|
get_base_fee_estimate.response["fee_per_output"] = fees.second;
|
|
get_base_fee_estimate.response["blink_fee_fixed"] = oxen::BLINK_BURN_FIXED;
|
|
constexpr auto blink_percent = oxen::BLINK_MINER_TX_FEE_PERCENT + oxen::BLINK_BURN_TX_FEE_PERCENT_V18;
|
|
get_base_fee_estimate.response["blink_fee_per_byte"] = fees.first * blink_percent / 100;
|
|
get_base_fee_estimate.response["blink_fee_per_output"] = fees.second * blink_percent / 100;
|
|
get_base_fee_estimate.response["quantization_mask"] = Blockchain::get_fee_quantization_mask();
|
|
get_base_fee_estimate.response["status"] = STATUS_OK;
|
|
}
|
|
//------------------------------------------------------------------------------------------------------------------------------
|
|
void core_rpc_server::invoke(GET_ALTERNATE_CHAINS& get_alternate_chains, rpc_context context)
|
|
{
|
|
try
|
|
{
|
|
std::vector<GET_ALTERNATE_CHAINS::chain_info> chains;
|
|
std::vector<std::pair<Blockchain::block_extended_info, std::vector<crypto::hash>>> alt_chains = m_core.get_blockchain_storage().get_alternative_chains();
|
|
for (const auto &i: alt_chains)
|
|
{
|
|
chains.push_back(GET_ALTERNATE_CHAINS::chain_info{tools::type_to_hex(get_block_hash(i.first.bl)), i.first.height, i.second.size(), i.first.cumulative_difficulty, {}, std::string()});
|
|
chains.back().block_hashes.reserve(i.second.size());
|
|
for (const crypto::hash &block_id: i.second)
|
|
chains.back().block_hashes.push_back(tools::type_to_hex(block_id));
|
|
if (i.first.height < i.second.size())
|
|
{
|
|
get_alternate_chains.response["status"] = "Error finding alternate chain attachment point";
|
|
return;
|
|
}
|
|
cryptonote::block main_chain_parent_block;
|
|
try { main_chain_parent_block = m_core.get_blockchain_storage().get_db().get_block_from_height(i.first.height - i.second.size()); }
|
|
catch (const std::exception &e) { get_alternate_chains.response["status"] = "Error finding alternate chain attachment point"; return; }
|
|
chains.back().main_chain_parent_block = tools::type_to_hex(get_block_hash(main_chain_parent_block));
|
|
}
|
|
get_alternate_chains.response["chains"] = chains;
|
|
get_alternate_chains.response["status"] = STATUS_OK;
|
|
}
|
|
catch (...)
|
|
{
|
|
get_alternate_chains.response["status"] = "Error retrieving alternate chains";
|
|
}
|
|
return;
|
|
}
|
|
//------------------------------------------------------------------------------------------------------------------------------
|
|
void core_rpc_server::invoke(GET_LIMIT& limit, rpc_context context)
|
|
{
|
|
limit.response = {
|
|
{"limit_down", epee::net_utils::connection_basic::get_rate_down_limit()},
|
|
{"limit_up", epee::net_utils::connection_basic::get_rate_up_limit()},
|
|
{"status", STATUS_OK}};
|
|
}
|
|
//------------------------------------------------------------------------------------------------------------------------------
|
|
void core_rpc_server::invoke(SET_LIMIT& limit, rpc_context context)
|
|
{
|
|
// -1 = reset to default
|
|
// 0 = do not modify
|
|
if (limit.request.limit_down != 0)
|
|
epee::net_utils::connection_basic::set_rate_down_limit(
|
|
limit.request.limit_down == -1 ? p2p::DEFAULT_LIMIT_RATE_DOWN : limit.request.limit_down);
|
|
|
|
if (limit.request.limit_up != 0)
|
|
epee::net_utils::connection_basic::set_rate_up_limit(
|
|
limit.request.limit_up == -1 ? p2p::DEFAULT_LIMIT_RATE_UP : limit.request.limit_up);
|
|
|
|
limit.response = {
|
|
{"limit_down", epee::net_utils::connection_basic::get_rate_down_limit()},
|
|
{"limit_up", epee::net_utils::connection_basic::get_rate_up_limit()},
|
|
{"status", STATUS_OK}};
|
|
}
|
|
//------------------------------------------------------------------------------------------------------------------------------
|
|
void core_rpc_server::invoke(OUT_PEERS& out_peers, rpc_context context)
|
|
{
|
|
if (out_peers.request.set)
|
|
m_p2p.change_max_out_public_peers(out_peers.request.out_peers);
|
|
out_peers.response["status"] = STATUS_OK;
|
|
}
|
|
//------------------------------------------------------------------------------------------------------------------------------
|
|
void core_rpc_server::invoke(IN_PEERS& in_peers, rpc_context context)
|
|
{
|
|
if (in_peers.request.set)
|
|
m_p2p.change_max_in_public_peers(in_peers.request.in_peers);
|
|
in_peers.response["status"] = STATUS_OK;
|
|
}
|
|
//------------------------------------------------------------------------------------------------------------------------------
|
|
void core_rpc_server::invoke(POP_BLOCKS& pop_blocks, rpc_context context)
|
|
{
|
|
m_core.get_blockchain_storage().pop_blocks(pop_blocks.request.nblocks);
|
|
|
|
pop_blocks.response["height"] = m_core.get_current_blockchain_height();
|
|
pop_blocks.response["status"] = STATUS_OK;
|
|
}
|
|
//------------------------------------------------------------------------------------------------------------------------------
|
|
void core_rpc_server::invoke(RELAY_TX& relay_tx, rpc_context context)
|
|
{
|
|
std::string status = "";
|
|
for (const auto& txid_hex: relay_tx.request.txids)
|
|
{
|
|
crypto::hash txid;
|
|
if (!tools::hex_to_type(txid_hex, txid))
|
|
{
|
|
if (!status.empty()) status += ", ";
|
|
status += "invalid transaction id: " + txid_hex;
|
|
continue;
|
|
}
|
|
|
|
if (std::string txblob; m_core.get_pool().get_transaction(txid, txblob))
|
|
{
|
|
cryptonote_connection_context fake_context{};
|
|
NOTIFY_NEW_TRANSACTIONS::request r{};
|
|
r.txs.push_back(txblob);
|
|
m_core.get_protocol()->relay_transactions(r, fake_context);
|
|
//TODO: make sure that tx has reached other nodes here, probably wait to receive reflections from other nodes
|
|
}
|
|
else
|
|
{
|
|
if (!status.empty()) status += ", ";
|
|
status += "transaction not found in pool: " + txid_hex;
|
|
}
|
|
}
|
|
|
|
if (status.empty())
|
|
status = STATUS_OK;
|
|
|
|
relay_tx.response["status"] = status;
|
|
return;
|
|
}
|
|
//------------------------------------------------------------------------------------------------------------------------------
|
|
void core_rpc_server::invoke(SYNC_INFO& sync, rpc_context context)
|
|
{
|
|
auto [top_height, top_hash] = m_core.get_blockchain_top();
|
|
sync.response["height"] = top_height + 1; // turn top block height into blockchain height
|
|
if (auto target_height = m_core.get_target_blockchain_height(); target_height > top_height + 1)
|
|
sync.response["target_height"] = target_height;
|
|
// Don't put this into the response until it actually does something on Oxen:
|
|
if (false)
|
|
sync.response["next_needed_pruning_seed"] = m_p2p.get_payload_object().get_next_needed_pruning_stripe().second;
|
|
|
|
auto& peers = sync.response["peers"];
|
|
peers = json{};
|
|
for (auto& ci : m_p2p.get_payload_object().get_connections())
|
|
peers[ci.connection_id] = json_connection_info(ci);
|
|
const auto& block_queue = m_p2p.get_payload_object().get_block_queue();
|
|
auto spans = json::array();
|
|
block_queue.foreach([&spans, &block_queue](const auto& span) {
|
|
uint32_t speed = (uint32_t)(100.0f * block_queue.get_speed(span.connection_id) + 0.5f);
|
|
spans.push_back(json{
|
|
{"start_block_height", span.start_block_height},
|
|
{"nblocks", span.nblocks},
|
|
{"connection_id", tools::type_to_hex(span.connection_id)},
|
|
{"rate", std::lround(span.rate)},
|
|
{"speed", speed},
|
|
{"size", span.size}});
|
|
return true;
|
|
});
|
|
sync.response["overview"] = block_queue.get_overview(top_height + 1);
|
|
sync.response["status"] = STATUS_OK;
|
|
}
|
|
|
|
namespace {
|
|
output_distribution_data process_distribution(
|
|
bool cumulative,
|
|
std::uint64_t start_height,
|
|
std::vector<std::uint64_t> distribution,
|
|
std::uint64_t base)
|
|
{
|
|
if (!cumulative && !distribution.empty())
|
|
{
|
|
for (std::size_t n = distribution.size() - 1; 0 < n; --n)
|
|
distribution[n] -= distribution[n - 1];
|
|
distribution[0] -= base;
|
|
}
|
|
|
|
return {std::move(distribution), start_height, base};
|
|
}
|
|
|
|
static struct {
|
|
std::mutex mutex;
|
|
std::vector<std::uint64_t> cached_distribution;
|
|
std::uint64_t cached_from = 0, cached_to = 0, cached_start_height = 0, cached_base = 0;
|
|
crypto::hash cached_m10_hash{};
|
|
crypto::hash cached_top_hash{};
|
|
bool cached = false;
|
|
} output_dist_cache;
|
|
}
|
|
|
|
namespace detail {
|
|
std::optional<output_distribution_data> get_output_distribution(
|
|
const std::function<bool(uint64_t, uint64_t, uint64_t, uint64_t&, std::vector<uint64_t>&, uint64_t&)>& f,
|
|
uint64_t amount,
|
|
uint64_t from_height,
|
|
uint64_t to_height,
|
|
const std::function<crypto::hash(uint64_t)>& get_hash,
|
|
bool cumulative,
|
|
uint64_t blockchain_height)
|
|
{
|
|
auto& d = output_dist_cache;
|
|
const std::unique_lock lock{d.mutex};
|
|
|
|
crypto::hash top_hash{};
|
|
if (d.cached_to < blockchain_height)
|
|
top_hash = get_hash(d.cached_to);
|
|
if (d.cached && amount == 0 && d.cached_from == from_height && d.cached_to == to_height && d.cached_top_hash == top_hash)
|
|
return process_distribution(cumulative, d.cached_start_height, d.cached_distribution, d.cached_base);
|
|
|
|
std::vector<std::uint64_t> distribution;
|
|
std::uint64_t start_height, base;
|
|
|
|
// see if we can extend the cache - a common case
|
|
bool can_extend = d.cached && amount == 0 && d.cached_from == from_height && to_height > d.cached_to && top_hash == d.cached_top_hash;
|
|
if (!can_extend)
|
|
{
|
|
// we kept track of the hash 10 blocks below, if it exists, so if it matches,
|
|
// we can still pop the last 10 cached slots and try again
|
|
if (d.cached && amount == 0 && d.cached_from == from_height && d.cached_to - d.cached_from >= 10 && to_height > d.cached_to - 10)
|
|
{
|
|
crypto::hash hash10 = get_hash(d.cached_to - 10);
|
|
if (hash10 == d.cached_m10_hash)
|
|
{
|
|
d.cached_to -= 10;
|
|
d.cached_top_hash = hash10;
|
|
d.cached_m10_hash = crypto::null<crypto::hash>;
|
|
CHECK_AND_ASSERT_MES(d.cached_distribution.size() >= 10, std::nullopt, "Cached distribution size does not match cached bounds");
|
|
for (int p = 0; p < 10; ++p)
|
|
d.cached_distribution.pop_back();
|
|
can_extend = true;
|
|
}
|
|
}
|
|
}
|
|
if (can_extend)
|
|
{
|
|
std::vector<std::uint64_t> new_distribution;
|
|
if (!f(amount, d.cached_to + 1, to_height, start_height, new_distribution, base))
|
|
return std::nullopt;
|
|
distribution = d.cached_distribution;
|
|
distribution.reserve(distribution.size() + new_distribution.size());
|
|
for (const auto &e: new_distribution)
|
|
distribution.push_back(e);
|
|
start_height = d.cached_start_height;
|
|
base = d.cached_base;
|
|
}
|
|
else
|
|
{
|
|
if (!f(amount, from_height, to_height, start_height, distribution, base))
|
|
return std::nullopt;
|
|
}
|
|
|
|
if (to_height > 0 && to_height >= from_height)
|
|
{
|
|
const std::uint64_t offset = std::max(from_height, start_height);
|
|
if (offset <= to_height && to_height - offset + 1 < distribution.size())
|
|
distribution.resize(to_height - offset + 1);
|
|
}
|
|
|
|
if (amount == 0)
|
|
{
|
|
d.cached_from = from_height;
|
|
d.cached_to = to_height;
|
|
d.cached_top_hash = get_hash(d.cached_to);
|
|
d.cached_m10_hash = d.cached_to >= 10 ? get_hash(d.cached_to - 10) : crypto::null<crypto::hash>;
|
|
d.cached_distribution = distribution;
|
|
d.cached_start_height = start_height;
|
|
d.cached_base = base;
|
|
d.cached = true;
|
|
}
|
|
|
|
return process_distribution(cumulative, start_height, std::move(distribution), base);
|
|
}
|
|
}
|
|
|
|
//------------------------------------------------------------------------------------------------------------------------------
|
|
GET_OUTPUT_DISTRIBUTION::response core_rpc_server::invoke(GET_OUTPUT_DISTRIBUTION::request&& req, rpc_context context, bool binary)
|
|
{
|
|
GET_OUTPUT_DISTRIBUTION::response res{};
|
|
try
|
|
{
|
|
// 0 is placeholder for the whole chain
|
|
const uint64_t req_to_height = req.to_height ? req.to_height : (m_core.get_current_blockchain_height() - 1);
|
|
for (uint64_t amount: req.amounts)
|
|
{
|
|
auto data = detail::get_output_distribution(
|
|
[this](auto&&... args) { return m_core.get_output_distribution(std::forward<decltype(args)>(args)...); },
|
|
amount,
|
|
req.from_height,
|
|
req_to_height,
|
|
[this](uint64_t height) { return m_core.get_blockchain_storage().get_db().get_block_hash_from_height(height); },
|
|
req.cumulative,
|
|
m_core.get_current_blockchain_height());
|
|
if (!data)
|
|
throw rpc_error{ERROR_INTERNAL, "Failed to get output distribution"};
|
|
|
|
// Force binary & compression off if this is a JSON request because trying to pass binary
|
|
// data through JSON explodes it in terms of size (most values under 0x20 have to be encoded
|
|
// using 6 chars such as "\u0002").
|
|
res.distributions.push_back({std::move(*data), amount, "", binary && req.binary, binary && req.compress});
|
|
}
|
|
}
|
|
catch (const std::exception &e)
|
|
{
|
|
throw rpc_error{ERROR_INTERNAL, "Failed to get output distribution"};
|
|
}
|
|
|
|
res.status = STATUS_OK;
|
|
return res;
|
|
}
|
|
//------------------------------------------------------------------------------------------------------------------------------
|
|
GET_OUTPUT_DISTRIBUTION_BIN::response core_rpc_server::invoke(GET_OUTPUT_DISTRIBUTION_BIN::request&& req, rpc_context context)
|
|
{
|
|
GET_OUTPUT_DISTRIBUTION_BIN::response res{};
|
|
|
|
|
|
if (!req.binary)
|
|
{
|
|
res.status = "Binary only call";
|
|
return res;
|
|
}
|
|
|
|
return invoke(std::move(static_cast<GET_OUTPUT_DISTRIBUTION::request&>(req)), context, true);
|
|
}
|
|
//------------------------------------------------------------------------------------------------------------------------------
|
|
void core_rpc_server::invoke(PRUNE_BLOCKCHAIN& prune_blockchain, rpc_context context)
|
|
{
|
|
try
|
|
{
|
|
if (!(prune_blockchain.request.check ? m_core.check_blockchain_pruning() : m_core.prune_blockchain()))
|
|
throw rpc_error{ERROR_INTERNAL, prune_blockchain.request.check ? "Failed to check blockchain pruning" : "Failed to prune blockchain"};
|
|
auto pruning_seed = m_core.get_blockchain_pruning_seed();
|
|
prune_blockchain.response["pruning_seed"] = pruning_seed;
|
|
prune_blockchain.response["pruned"] = pruning_seed != 0;
|
|
}
|
|
catch (const std::exception &e)
|
|
{
|
|
throw rpc_error{ERROR_INTERNAL, "Failed to prune blockchain"};
|
|
}
|
|
|
|
prune_blockchain.response["status"] = STATUS_OK;
|
|
}
|
|
|
|
|
|
//------------------------------------------------------------------------------------------------------------------------------
|
|
void core_rpc_server::invoke(GET_QUORUM_STATE& get_quorum_state, rpc_context context)
|
|
{
|
|
const auto& quorum_type = get_quorum_state.request.quorum_type;
|
|
|
|
auto is_requested_type = [&quorum_type](service_nodes::quorum_type type) {
|
|
return !quorum_type || quorum_type == static_cast<uint8_t>(type);
|
|
};
|
|
|
|
bool latest = false;
|
|
uint64_t latest_ob = 0, latest_cp = 0, latest_bl = 0;
|
|
auto& start = get_quorum_state.request.start_height;
|
|
auto& end = get_quorum_state.request.end_height;
|
|
uint64_t curr_height = m_core.get_blockchain_storage().get_current_blockchain_height();
|
|
if (!start && !end)
|
|
{
|
|
latest = true;
|
|
// Our start block for the latest quorum of each type depends on the type being requested:
|
|
// obligations: top block
|
|
// checkpoint: last block with height divisible by CHECKPOINT_INTERVAL (=4)
|
|
// blink: last block with height divisible by BLINK_QUORUM_INTERVAL (=5)
|
|
// pulse: current height (i.e. top block height + 1)
|
|
uint64_t top_height = curr_height - 1;
|
|
latest_ob = top_height;
|
|
latest_cp = top_height - top_height % service_nodes::CHECKPOINT_INTERVAL;
|
|
latest_bl = top_height - top_height % service_nodes::BLINK_QUORUM_INTERVAL;
|
|
if (is_requested_type(service_nodes::quorum_type::checkpointing))
|
|
start = latest_cp;
|
|
if (is_requested_type(service_nodes::quorum_type::blink))
|
|
start = start ? std::min(*start, latest_bl) : latest_bl;
|
|
end = curr_height;
|
|
}
|
|
else if (!start)
|
|
start = (*end)++;
|
|
else if (!end)
|
|
end = *start + 1;
|
|
else if (*end > *start)
|
|
++*end;
|
|
else if (end > 0)
|
|
--*end;
|
|
|
|
if (!start || *start > curr_height)
|
|
start = curr_height;
|
|
|
|
// We can also provide the pulse quorum for the current block being produced, so if asked for
|
|
// that make a note.
|
|
bool add_curr_pulse = (latest || end > curr_height) && is_requested_type(service_nodes::quorum_type::pulse);
|
|
if (!end || *end > curr_height)
|
|
end = curr_height;
|
|
|
|
uint64_t count = (*start > *end) ? *start - *end : *end - *start;
|
|
if (!context.admin && count > GET_QUORUM_STATE::MAX_COUNT)
|
|
throw rpc_error{ERROR_WRONG_PARAM,
|
|
"Number of requested quorums greater than the allowed limit: "
|
|
+ std::to_string(GET_QUORUM_STATE::MAX_COUNT)
|
|
+ ", requested: " + std::to_string(count)};
|
|
|
|
bool at_least_one_succeeded = false;
|
|
std::vector<GET_QUORUM_STATE::quorum_for_height> quorums;
|
|
quorums.reserve(std::min((uint64_t)16, count));
|
|
auto net = nettype();
|
|
for (size_t height = *start; height < *end; height++)
|
|
{
|
|
auto hf_version = get_network_version(net, height);
|
|
auto start_quorum_iterator = static_cast<service_nodes::quorum_type>(0);
|
|
auto end_quorum_iterator = service_nodes::max_quorum_type_for_hf(hf_version);
|
|
|
|
if (quorum_type)
|
|
{
|
|
start_quorum_iterator = static_cast<service_nodes::quorum_type>(*quorum_type);
|
|
end_quorum_iterator = start_quorum_iterator;
|
|
}
|
|
|
|
for (int quorum_int = (int)start_quorum_iterator; quorum_int <= (int)end_quorum_iterator; quorum_int++)
|
|
{
|
|
auto type = static_cast<service_nodes::quorum_type>(quorum_int);
|
|
if (latest)
|
|
{ // Latest quorum requested, so skip if this is isn't the latest height for *this* quorum type
|
|
if (type == service_nodes::quorum_type::obligations && height != latest_ob) continue;
|
|
if (type == service_nodes::quorum_type::checkpointing && height != latest_cp) continue;
|
|
if (type == service_nodes::quorum_type::blink && height != latest_bl) continue;
|
|
if (type == service_nodes::quorum_type::pulse) continue;
|
|
}
|
|
if (std::shared_ptr<const service_nodes::quorum> quorum = m_core.get_quorum(type, height, true /*include_old*/))
|
|
{
|
|
auto& entry = quorums.emplace_back();
|
|
entry.height = height;
|
|
entry.quorum_type = static_cast<uint8_t>(quorum_int);
|
|
entry.quorum.validators = hexify(quorum->validators);
|
|
entry.quorum.workers = hexify(quorum->workers);
|
|
|
|
at_least_one_succeeded = true;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (auto hf_version = get_network_version(nettype(), curr_height);
|
|
add_curr_pulse && hf_version >= hf::hf16_pulse)
|
|
{
|
|
const auto& blockchain = m_core.get_blockchain_storage();
|
|
const auto& top_header = blockchain.get_db().get_block_header_from_height(curr_height - 1);
|
|
|
|
pulse::timings next_timings{};
|
|
uint8_t pulse_round = 0;
|
|
if (pulse::get_round_timings(blockchain, curr_height, top_header.timestamp, next_timings) &&
|
|
pulse::convert_time_to_round(pulse::clock::now(), next_timings.r0_timestamp, &pulse_round))
|
|
{
|
|
auto entropy = service_nodes::get_pulse_entropy_for_next_block(blockchain.get_db(), pulse_round);
|
|
auto& sn_list = m_core.get_service_node_list();
|
|
auto quorum = generate_pulse_quorum(m_core.get_nettype(), sn_list.get_block_leader().key, hf_version, sn_list.active_service_nodes_infos(), entropy, pulse_round);
|
|
if (verify_pulse_quorum_sizes(quorum))
|
|
{
|
|
auto& entry = quorums.emplace_back();
|
|
entry.height = curr_height;
|
|
entry.quorum_type = static_cast<uint8_t>(service_nodes::quorum_type::pulse);
|
|
|
|
entry.quorum.validators = hexify(quorum.validators);
|
|
entry.quorum.workers = hexify(quorum.workers);
|
|
|
|
at_least_one_succeeded = true;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (!at_least_one_succeeded)
|
|
throw rpc_error{ERROR_WRONG_PARAM, "Failed to query any quorums at all"};
|
|
|
|
get_quorum_state.response["quorums"] = quorums;
|
|
get_quorum_state.response["status"] = STATUS_OK;
|
|
return;
|
|
}
|
|
//------------------------------------------------------------------------------------------------------------------------------
|
|
void core_rpc_server::invoke(FLUSH_CACHE& flush_cache, rpc_context context)
|
|
{
|
|
if (flush_cache.request.bad_txs)
|
|
m_core.flush_bad_txs_cache();
|
|
if (flush_cache.request.bad_blocks)
|
|
m_core.flush_invalid_blocks();
|
|
flush_cache.response["status"] = STATUS_OK;
|
|
}
|
|
//------------------------------------------------------------------------------------------------------------------------------
|
|
void core_rpc_server::invoke(GET_SERVICE_NODE_REGISTRATION_CMD_RAW& get_service_node_registration_cmd_raw, rpc_context context)
|
|
{
|
|
if (!m_core.service_node())
|
|
throw rpc_error{ERROR_WRONG_PARAM, "Daemon has not been started in service node mode, please relaunch with --service-node flag."};
|
|
|
|
auto hf_version = get_network_version(nettype(), m_core.get_current_blockchain_height());
|
|
std::string registration_cmd;
|
|
if (!service_nodes::make_registration_cmd(m_core.get_nettype(),
|
|
hf_version,
|
|
get_service_node_registration_cmd_raw.request.staking_requirement,
|
|
get_service_node_registration_cmd_raw.request.args,
|
|
m_core.get_service_keys(),
|
|
registration_cmd,
|
|
get_service_node_registration_cmd_raw.request.make_friendly))
|
|
throw rpc_error{ERROR_INTERNAL, "Failed to make registration command"};
|
|
|
|
get_service_node_registration_cmd_raw.response["registration_cmd"] = registration_cmd;
|
|
get_service_node_registration_cmd_raw.response["status"] = STATUS_OK;
|
|
return;
|
|
}
|
|
//------------------------------------------------------------------------------------------------------------------------------
|
|
GET_SERVICE_NODE_REGISTRATION_CMD::response core_rpc_server::invoke(GET_SERVICE_NODE_REGISTRATION_CMD::request&& req, rpc_context context)
|
|
{
|
|
GET_SERVICE_NODE_REGISTRATION_CMD::response res{};
|
|
|
|
std::vector<std::string> args;
|
|
|
|
std::optional<uint64_t> height = m_core.get_current_blockchain_height();
|
|
auto hf_version = get_network_version(nettype(), *height);
|
|
uint64_t staking_requirement = service_nodes::get_staking_requirement(nettype(), *height);
|
|
|
|
{
|
|
try {
|
|
args.emplace_back(std::to_string(service_nodes::percent_to_basis_points(req.operator_cut)));
|
|
} catch(const std::exception &e) {
|
|
res.status = "Invalid value: "s + e.what();
|
|
log::error(logcat, res.status);
|
|
return res;
|
|
}
|
|
}
|
|
|
|
for (const auto& [address, amount] : req.contributions)
|
|
{
|
|
args.push_back(address);
|
|
args.push_back(std::to_string(amount));
|
|
}
|
|
|
|
GET_SERVICE_NODE_REGISTRATION_CMD_RAW req_old{};
|
|
|
|
req_old.request.staking_requirement = req.staking_requirement;
|
|
req_old.request.args = std::move(args);
|
|
req_old.request.make_friendly = false;
|
|
|
|
invoke(req_old, context);
|
|
res.status = req_old.response["status"];
|
|
res.registration_cmd = req_old.response["registration_cmd"];
|
|
return res;
|
|
}
|
|
//------------------------------------------------------------------------------------------------------------------------------
|
|
void core_rpc_server::invoke(GET_SERVICE_NODE_BLACKLISTED_KEY_IMAGES& get_service_node_blacklisted_key_images, rpc_context context)
|
|
{
|
|
auto &blacklist = m_core.get_service_node_blacklisted_key_images();
|
|
|
|
get_service_node_blacklisted_key_images.response["status"] = STATUS_OK;
|
|
get_service_node_blacklisted_key_images.response["blacklist"] = blacklist;
|
|
return;
|
|
}
|
|
//------------------------------------------------------------------------------------------------------------------------------
|
|
void core_rpc_server::invoke(GET_SERVICE_KEYS& get_service_keys, rpc_context context)
|
|
{
|
|
const auto& keys = m_core.get_service_keys();
|
|
if (keys.pub)
|
|
get_service_keys.response["service_node_pubkey"] = tools::type_to_hex(keys.pub);
|
|
get_service_keys.response["service_node_ed25519_pubkey"] = tools::type_to_hex(keys.pub_ed25519);
|
|
get_service_keys.response["service_node_x25519_pubkey"] = tools::type_to_hex(keys.pub_x25519);
|
|
get_service_keys.response["status"] = STATUS_OK;
|
|
return;
|
|
}
|
|
//------------------------------------------------------------------------------------------------------------------------------
|
|
void core_rpc_server::invoke(GET_SERVICE_PRIVKEYS& get_service_privkeys, rpc_context context)
|
|
{
|
|
const auto& keys = m_core.get_service_keys();
|
|
if (keys.key)
|
|
get_service_privkeys.response["service_node_privkey"] = tools::type_to_hex(keys.key);
|
|
get_service_privkeys.response["service_node_ed25519_privkey"] = tools::type_to_hex(keys.key_ed25519);
|
|
get_service_privkeys.response["service_node_x25519_privkey"] = tools::type_to_hex(keys.key_x25519);
|
|
get_service_privkeys.response["status"] = STATUS_OK;
|
|
return;
|
|
}
|
|
|
|
static time_t reachable_to_time_t(
|
|
std::chrono::steady_clock::time_point t,
|
|
std::chrono::system_clock::time_point system_now,
|
|
std::chrono::steady_clock::time_point steady_now) {
|
|
if (t == service_nodes::NEVER)
|
|
return 0;
|
|
return std::chrono::system_clock::to_time_t(
|
|
std::chrono::time_point_cast<std::chrono::system_clock::duration>(
|
|
system_now + (t - steady_now)));
|
|
}
|
|
|
|
static bool requested(const std::unordered_set<std::string>& requested, const std::string& key) {
|
|
return requested.empty() ||
|
|
(requested.count("all")
|
|
? !requested.count("-" + key)
|
|
: requested.count(key));
|
|
}
|
|
|
|
template <typename Dict, typename T, typename... More>
|
|
static void set_if_requested(const std::unordered_set<std::string>& reqed, Dict& dict,
|
|
const std::string& key, T&& value, More&&... more) {
|
|
if (requested(reqed, key))
|
|
dict[key] = std::forward<T>(value);
|
|
if constexpr (sizeof...(More) > 0)
|
|
set_if_requested(reqed, dict, std::forward<More>(more)...);
|
|
}
|
|
|
|
//------------------------------------------------------------------------------------------------------------------------------
|
|
void core_rpc_server::fill_sn_response_entry(
|
|
json& entry,
|
|
bool is_bt,
|
|
const std::unordered_set<std::string>& reqed,
|
|
const service_nodes::service_node_pubkey_info& sn_info,
|
|
uint64_t top_height) {
|
|
|
|
auto binary_format = is_bt ? json_binary_proxy::fmt::bt : json_binary_proxy::fmt::hex;
|
|
json_binary_proxy binary{entry, binary_format};
|
|
|
|
const auto &info = *sn_info.info;
|
|
set_if_requested(reqed, binary, "service_node_pubkey", sn_info.pubkey);
|
|
set_if_requested(reqed, entry,
|
|
"registration_height", info.registration_height,
|
|
"requested_unlock_height", info.requested_unlock_height,
|
|
"last_reward_block_height", info.last_reward_block_height,
|
|
"last_reward_transaction_index", info.last_reward_transaction_index,
|
|
"active", info.is_active(),
|
|
"funded", info.is_fully_funded(),
|
|
"state_height", info.is_fully_funded()
|
|
? (info.is_decommissioned() ? info.last_decommission_height : info.active_since_height)
|
|
: info.last_reward_block_height,
|
|
"earned_downtime_blocks", service_nodes::quorum_cop::calculate_decommission_credit(info, top_height),
|
|
"decommission_count", info.decommission_count,
|
|
"total_contributed", info.total_contributed,
|
|
"staking_requirement", info.staking_requirement,
|
|
"portions_for_operator", info.portions_for_operator,
|
|
"operator_fee", microportion(info.portions_for_operator),
|
|
"operator_address", cryptonote::get_account_address_as_str(m_core.get_nettype(), false/*subaddress*/, info.operator_address),
|
|
"swarm_id", info.swarm_id,
|
|
"swarm", "{:x}"_format(info.swarm_id),
|
|
"registration_hf_version", info.registration_hf_version
|
|
);
|
|
|
|
if (requested(reqed, "total_reserved") && info.total_reserved != info.total_contributed)
|
|
entry["total_reserved"] = info.total_reserved;
|
|
|
|
if (info.last_decommission_reason_consensus_any) {
|
|
set_if_requested(reqed, entry,
|
|
"last_decommission_reason_consensus_all", info.last_decommission_reason_consensus_all,
|
|
"last_decommission_reason_consensus_any", info.last_decommission_reason_consensus_any);
|
|
|
|
if (requested(reqed, "last_decomm_reasons")) {
|
|
auto& reasons = (entry["last_decomm_reasons"] = json{
|
|
{"all", cryptonote::coded_reasons(info.last_decommission_reason_consensus_all)}});
|
|
if (auto some = cryptonote::coded_reasons(info.last_decommission_reason_consensus_any & ~info.last_decommission_reason_consensus_all);
|
|
!some.empty())
|
|
reasons["some"] = std::move(some);
|
|
}
|
|
}
|
|
|
|
auto& netconf = m_core.get_net_config();
|
|
// FIXME: accessing proofs one-by-one like this is kind of gross.
|
|
m_core.get_service_node_list().access_proof(sn_info.pubkey, [&](const auto& proof) {
|
|
if (m_core.service_node() && m_core.get_service_keys().pub == sn_info.pubkey) {
|
|
// When returning our own info we always want to return the most current data because the
|
|
// data from the SN list could be stale (it only gets updated when we get verification of
|
|
// acceptance of our proof from the network). The rest of the network might not get the
|
|
// updated data until the next proof, but local callers like SS and Lokinet want it updated
|
|
// immediately.
|
|
set_if_requested(reqed, entry,
|
|
"service_node_version", OXEN_VERSION,
|
|
"lokinet_version", m_core.lokinet_version,
|
|
"storage_server_version", m_core.ss_version,
|
|
"public_ip", epee::string_tools::get_ip_string_from_int32(m_core.sn_public_ip()),
|
|
"storage_port", m_core.storage_https_port(),
|
|
"storage_lmq_port", m_core.storage_omq_port(),
|
|
"quorumnet_port", m_core.quorumnet_port());
|
|
set_if_requested(reqed, binary,
|
|
"pubkey_ed25519", m_core.get_service_keys().pub_ed25519,
|
|
"pubkey_x25519", m_core.get_service_keys().pub_x25519);
|
|
} else {
|
|
if (proof.proof->public_ip != 0)
|
|
set_if_requested(reqed, entry,
|
|
"service_node_version", proof.proof->version,
|
|
"lokinet_version", proof.proof->lokinet_version,
|
|
"storage_server_version", proof.proof->storage_server_version,
|
|
"public_ip", epee::string_tools::get_ip_string_from_int32(proof.proof->public_ip),
|
|
"storage_port", proof.proof->storage_https_port,
|
|
"storage_lmq_port", proof.proof->storage_omq_port,
|
|
"quorumnet_port", proof.proof->qnet_port);
|
|
if (proof.proof->pubkey_ed25519)
|
|
set_if_requested(reqed, binary,
|
|
"pubkey_ed25519", proof.proof->pubkey_ed25519,
|
|
"pubkey_x25519", proof.pubkey_x25519);
|
|
}
|
|
|
|
auto system_now = std::chrono::system_clock::now();
|
|
auto steady_now = std::chrono::steady_clock::now();
|
|
set_if_requested(reqed, entry, "last_uptime_proof", proof.timestamp);
|
|
if (m_core.service_node()) {
|
|
set_if_requested(reqed, entry,
|
|
"storage_server_reachable", !proof.ss_reachable.unreachable_for(netconf.UPTIME_PROOF_VALIDITY - netconf.UPTIME_PROOF_FREQUENCY, steady_now),
|
|
"lokinet_reachable", !proof.lokinet_reachable.unreachable_for(netconf.UPTIME_PROOF_VALIDITY - netconf.UPTIME_PROOF_FREQUENCY, steady_now));
|
|
if (proof.ss_reachable.first_unreachable != service_nodes::NEVER && requested(reqed, "storage_server_first_unreachable"))
|
|
entry["storage_server_first_unreachable"] = reachable_to_time_t(proof.ss_reachable.first_unreachable, system_now, steady_now);
|
|
if (proof.ss_reachable.last_unreachable != service_nodes::NEVER && requested(reqed, "storage_server_last_unreachable"))
|
|
entry["storage_server_last_unreachable"] = reachable_to_time_t(proof.ss_reachable.last_unreachable, system_now, steady_now);
|
|
if (proof.ss_reachable.last_reachable != service_nodes::NEVER && requested(reqed, "storage_server_last_reachable"))
|
|
entry["storage_server_last_reachable"] = reachable_to_time_t(proof.ss_reachable.last_reachable, system_now, steady_now);
|
|
if (proof.lokinet_reachable.first_unreachable != service_nodes::NEVER && requested(reqed, "lokinet_first_unreachable"))
|
|
entry["lokinet_first_unreachable"] = reachable_to_time_t(proof.lokinet_reachable.first_unreachable, system_now, steady_now);
|
|
if (proof.lokinet_reachable.last_unreachable != service_nodes::NEVER && requested(reqed, "lokinet_last_unreachable"))
|
|
entry["lokinet_last_unreachable"] = reachable_to_time_t(proof.lokinet_reachable.last_unreachable, system_now, steady_now);
|
|
if (proof.lokinet_reachable.last_reachable != service_nodes::NEVER && requested(reqed, "lokinet_last_reachable"))
|
|
entry["lokinet_last_reachable"] = reachable_to_time_t(proof.lokinet_reachable.last_reachable, system_now, steady_now);
|
|
}
|
|
|
|
if (requested(reqed, "checkpoint_votes") && !proof.checkpoint_participation.empty()) {
|
|
std::vector<uint64_t> voted, missed;
|
|
for (auto& cpp : proof.checkpoint_participation)
|
|
(cpp.pass() ? voted : missed).push_back(cpp.height);
|
|
std::sort(voted.begin(), voted.end());
|
|
std::sort(missed.begin(), missed.end());
|
|
entry["checkpoint_votes"] = json{
|
|
{"voted", voted},
|
|
{"missed", missed}};
|
|
}
|
|
if (requested(reqed, "pulse_votes") && !proof.pulse_participation.empty()) {
|
|
std::vector<std::pair<uint64_t, uint8_t>> voted, missed;
|
|
for (auto& ppp : proof.pulse_participation)
|
|
(ppp.pass() ? voted : missed).emplace_back(ppp.height, ppp.round);
|
|
std::sort(voted.begin(), voted.end());
|
|
std::sort(missed.begin(), missed.end());
|
|
entry["pulse_votes"]["voted"] = voted;
|
|
entry["pulse_votes"]["missed"] = missed;
|
|
}
|
|
if (requested(reqed, "quorumnet_tests") && !proof.timestamp_participation.empty()) {
|
|
auto fails = proof.timestamp_participation.failures();
|
|
entry["quorumnet_tests"] = json::array({proof.timestamp_participation.size() - fails, fails});
|
|
}
|
|
if (requested(reqed, "timesync_tests") && !proof.timesync_status.empty()) {
|
|
auto fails = proof.timesync_status.failures();
|
|
entry["timesync_tests"] = json::array({proof.timesync_status.size() - fails, fails});
|
|
}
|
|
});
|
|
|
|
if (requested(reqed, "contributors")) {
|
|
auto& contributors = (entry["contributors"] = json::array());
|
|
for (const auto& contributor : info.contributors) {
|
|
auto& c = contributors.emplace_back(json{
|
|
{"amount", contributor.amount},
|
|
{"address", cryptonote::get_account_address_as_str(m_core.get_nettype(), false/*subaddress*/, contributor.address)}});
|
|
if (contributor.reserved != contributor.amount)
|
|
c["reserved"] = contributor.reserved;
|
|
if (requested(reqed, "locked_contributions")) {
|
|
auto& locked = (c["locked_contributions"] = json::array());
|
|
for (const auto& src : contributor.locked_contributions) {
|
|
auto& lc = locked.emplace_back(json{{"amount", src.amount}});
|
|
json_binary_proxy lc_binary{lc, binary_format};
|
|
lc_binary["key_image"] = src.key_image;
|
|
lc_binary["key_image_pub_key"] = src.key_image_pub_key;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
//------------------------------------------------------------------------------------------------------------------------------
|
|
void core_rpc_server::invoke(GET_SERVICE_NODES& sns, rpc_context context)
|
|
{
|
|
auto& req = sns.request;
|
|
sns.response["status"] = STATUS_OK;
|
|
auto [top_height, top_hash] = m_core.get_blockchain_top();
|
|
auto [hf, snode_rev] = get_network_version_revision(nettype(), top_height);
|
|
set_if_requested(req.fields, sns.response,
|
|
"height", top_height,
|
|
"target_height", m_core.get_target_blockchain_height(),
|
|
"hardfork", hf,
|
|
"snode_revision", snode_rev);
|
|
set_if_requested(req.fields, sns.response_hex,
|
|
"block_hash", top_hash);
|
|
|
|
if (req.poll_block_hash) {
|
|
bool unchanged = req.poll_block_hash == top_hash;
|
|
sns.response["unchanged"] = unchanged;
|
|
if (unchanged)
|
|
return;
|
|
if (!requested(req.fields, "block_hash"))
|
|
sns.response_hex["block_hash"] = top_hash; // Force it on a poll request even if it wasn't a requested field
|
|
}
|
|
|
|
auto sn_infos = m_core.get_service_node_list_state(req.service_node_pubkeys);
|
|
|
|
if (req.active_only)
|
|
sn_infos.erase(
|
|
std::remove_if(sn_infos.begin(), sn_infos.end(), [](const service_nodes::service_node_pubkey_info& snpk_info) {
|
|
return !snpk_info.info->is_active();
|
|
}),
|
|
sn_infos.end());
|
|
|
|
const int top_sn_index = (int) sn_infos.size() - 1;
|
|
if (req.limit < 0 || req.limit > top_sn_index) {
|
|
// We asked for -1 (no limit but shuffle) or a value >= the count, so just shuffle the entire list
|
|
std::shuffle(sn_infos.begin(), sn_infos.end(), tools::rng);
|
|
} else if (req.limit > 0) {
|
|
// We need to select N random elements, in random order, from yyyyyyyy. We could (and used
|
|
// to) just shuffle the entire list and return the first N, but that is quite inefficient when
|
|
// the list is large and N is small. So instead this algorithm is going to select a random
|
|
// element from yyyyyyyy, swap it to position 0, so we get: [x]yyyyyyyy where one of the new
|
|
// y's used to be at element 0. Then we select a random element from the new y's (i.e. all
|
|
// the elements beginning at position 1), and swap it into element 1, to get [xx]yyyyyy, then
|
|
// keep repeating until our set of x's is big enough, say [xxx]yyyyy. At that point we chop
|
|
// of the y's to just be left with [xxx], and only required N swaps in total.
|
|
for (int i = 0; i < req.limit; i++)
|
|
{
|
|
int j = std::uniform_int_distribution<int>{i, top_sn_index}(tools::rng);
|
|
using std::swap;
|
|
if (i != j)
|
|
swap(sn_infos[i], sn_infos[j]);
|
|
}
|
|
|
|
sn_infos.resize(req.limit);
|
|
}
|
|
|
|
auto& sn_states = (sns.response["service_node_states"] = json::array());
|
|
for (auto &pubkey_info : sn_infos)
|
|
fill_sn_response_entry(sn_states.emplace_back(json::object()), sns.is_bt(), req.fields, pubkey_info, top_height);
|
|
}
|
|
|
|
namespace {
|
|
// Handles a ping. Returns true if the ping was significant (i.e. first ping after startup, or
|
|
// after the ping had expired). `Success` is a callback that is invoked with a single boolean
|
|
// argument: true if this ping should trigger an immediate proof send (i.e. first ping after
|
|
// startup or after a ping expiry), false for an ordinary ping.
|
|
template <typename Success>
|
|
std::string handle_ping(
|
|
core& core,
|
|
std::array<uint16_t, 3> cur_version,
|
|
std::array<uint16_t, 3> required,
|
|
std::string_view ed25519_pubkey,
|
|
std::string_view error,
|
|
std::string_view name,
|
|
std::atomic<std::time_t>& update,
|
|
std::chrono::seconds lifetime,
|
|
Success success)
|
|
{
|
|
std::string status{};
|
|
std::string our_ed25519_pubkey = tools::type_to_hex(core.get_service_keys().pub_ed25519);
|
|
if (!error.empty()) {
|
|
status = fmt::format("Error: {}", error);
|
|
log::error(logcat, "{0} reported an error: {1}. Check {0} logs for more details.", name, error);
|
|
update = 0; // Reset our last ping time to 0 so that we won't send a ping until we get
|
|
// success back again (even if we had an earlier acceptable ping within the
|
|
// cutoff time).
|
|
} else if (cur_version < required) {
|
|
status = "Outdated {}. Current: {} Required: {}"_format(
|
|
name, fmt::join(cur_version, "."), fmt::join(required, "."));
|
|
log::error(logcat, status);
|
|
} else if (!ed25519_pubkey.empty() // TODO: once lokinet & ss are always sending this we can remove this empty bypass
|
|
&& ed25519_pubkey != our_ed25519_pubkey) {
|
|
status = "Invalid {} pubkey: expected {}, received {}"_format(name, our_ed25519_pubkey, ed25519_pubkey);
|
|
log::error(logcat, status);
|
|
} else {
|
|
auto now = std::time(nullptr);
|
|
auto old = update.exchange(now);
|
|
bool significant = std::chrono::seconds{now - old} > lifetime; // Print loudly for the first ping after startup/expiry
|
|
auto msg = "Received ping from {} {}"_format(name, fmt::join(cur_version, "."));
|
|
if (significant)
|
|
log::info(logcat, fg(fmt::terminal_color::green), "{}", msg);
|
|
else
|
|
log::debug(logcat, msg);
|
|
success(significant);
|
|
status = STATUS_OK;
|
|
}
|
|
return status;
|
|
}
|
|
}
|
|
|
|
//------------------------------------------------------------------------------------------------------------------------------
|
|
void core_rpc_server::invoke(STORAGE_SERVER_PING& storage_server_ping, rpc_context context)
|
|
{
|
|
m_core.ss_version = storage_server_ping.request.version;
|
|
storage_server_ping.response["status"] = handle_ping(m_core,
|
|
storage_server_ping.request.version, service_nodes::MIN_STORAGE_SERVER_VERSION,
|
|
storage_server_ping.request.ed25519_pubkey,
|
|
storage_server_ping.request.error,
|
|
"Storage Server", m_core.m_last_storage_server_ping, m_core.get_net_config().UPTIME_PROOF_FREQUENCY,
|
|
[this, &storage_server_ping](bool significant) {
|
|
m_core.m_storage_https_port = storage_server_ping.request.https_port;
|
|
m_core.m_storage_omq_port = storage_server_ping.request.omq_port;
|
|
if (significant)
|
|
m_core.reset_proof_interval();
|
|
});
|
|
}
|
|
//------------------------------------------------------------------------------------------------------------------------------
|
|
void core_rpc_server::invoke(LOKINET_PING& lokinet_ping, rpc_context context)
|
|
{
|
|
m_core.lokinet_version = lokinet_ping.request.version;
|
|
lokinet_ping.response["status"] = handle_ping(m_core,
|
|
lokinet_ping.request.version, service_nodes::MIN_LOKINET_VERSION,
|
|
lokinet_ping.request.ed25519_pubkey,
|
|
lokinet_ping.request.error,
|
|
"Lokinet", m_core.m_last_lokinet_ping, m_core.get_net_config().UPTIME_PROOF_FREQUENCY,
|
|
[this](bool significant) { if (significant) m_core.reset_proof_interval(); });
|
|
}
|
|
//------------------------------------------------------------------------------------------------------------------------------
|
|
void core_rpc_server::invoke(GET_STAKING_REQUIREMENT& get_staking_requirement, rpc_context context)
|
|
{
|
|
get_staking_requirement.response["height"] = get_staking_requirement.request.height > 0 ? get_staking_requirement.request.height : m_core.get_current_blockchain_height();
|
|
get_staking_requirement.response["staking_requirement"] = service_nodes::get_staking_requirement(nettype(), get_staking_requirement.request.height);
|
|
get_staking_requirement.response["status"] = STATUS_OK;
|
|
return;
|
|
}
|
|
|
|
template <typename T>
|
|
static void check_quantity_limit(T count, T max, const char* container_name = "input")
|
|
{
|
|
if (count > max)
|
|
throw rpc_error{ERROR_WRONG_PARAM,
|
|
"Number of requested entries ({}) in {} is greater than the allowed limit ({})"_format(
|
|
count, container_name, max)};
|
|
}
|
|
|
|
template <typename T>
|
|
static void check_quantity_limit(std::optional<T> count, T max, const char* name = "input") {
|
|
if (count)
|
|
check_quantity_limit(*count, max, name);
|
|
}
|
|
|
|
//------------------------------------------------------------------------------------------------------------------------------
|
|
void core_rpc_server::invoke(GET_CHECKPOINTS& get_checkpoints, rpc_context context)
|
|
{
|
|
if (!context.admin)
|
|
check_quantity_limit(get_checkpoints.request.count, GET_CHECKPOINTS::MAX_COUNT);
|
|
|
|
auto& start = get_checkpoints.request.start_height;
|
|
auto& end = get_checkpoints.request.end_height;
|
|
auto count = get_checkpoints.request.count.value_or(GET_CHECKPOINTS::NUM_CHECKPOINTS_TO_QUERY_BY_DEFAULT);
|
|
|
|
get_checkpoints.response["status"] = STATUS_OK;
|
|
const auto& db = m_core.get_blockchain_storage().get_db();
|
|
|
|
std::vector<checkpoint_t> checkpoints;
|
|
if (!start && !end)
|
|
{
|
|
if (checkpoint_t top_checkpoint; db.get_top_checkpoint(top_checkpoint))
|
|
checkpoints = db.get_checkpoints_range(top_checkpoint.height, 0, count);
|
|
}
|
|
else if (!start)
|
|
checkpoints = db.get_checkpoints_range(*end, 0, count);
|
|
else if (!end)
|
|
checkpoints = db.get_checkpoints_range(*start, UINT64_MAX, count);
|
|
else
|
|
checkpoints =
|
|
context.admin
|
|
? db.get_checkpoints_range(*start, *end)
|
|
: db.get_checkpoints_range(*start, *end, GET_CHECKPOINTS::MAX_COUNT);
|
|
|
|
get_checkpoints.response["checkpoints"] = std::move(checkpoints);
|
|
|
|
return;
|
|
}
|
|
//------------------------------------------------------------------------------------------------------------------------------
|
|
void core_rpc_server::invoke(GET_SN_STATE_CHANGES& get_sn_state_changes, rpc_context context)
|
|
{
|
|
using blob_t = std::string;
|
|
using block_pair_t = std::pair<blob_t, block>;
|
|
std::vector<block_pair_t> blocks;
|
|
|
|
const auto& db = m_core.get_blockchain_storage();
|
|
|
|
auto start_height = get_sn_state_changes.request.start_height;
|
|
auto end_height = get_sn_state_changes.request.end_height.value_or(db.get_current_blockchain_height() - 1);
|
|
|
|
if (end_height < start_height)
|
|
throw rpc_error{ERROR_WRONG_PARAM, "The provided end_height needs to be higher than start_height"};
|
|
|
|
if (!db.get_blocks(start_height, end_height - start_height + 1, blocks))
|
|
throw rpc_error{ERROR_INTERNAL, "Could not query blocks at requested height {}"_format(start_height)};
|
|
|
|
get_sn_state_changes.response["start_height"] = start_height;
|
|
get_sn_state_changes.response["end_height"] = end_height;
|
|
|
|
std::vector<blob_t> blobs;
|
|
int total_deregister = 0, total_decommission = 0, total_recommission = 0, total_ip_change_penalty = 0, total_unlock = 0;
|
|
for (const auto& block : blocks)
|
|
{
|
|
blobs.clear();
|
|
if (!db.get_transactions_blobs(block.second.tx_hashes, blobs))
|
|
{
|
|
log::error(logcat, "Could not query block at requested height: {}", cryptonote::get_block_height(block.second));
|
|
continue;
|
|
}
|
|
const auto hard_fork_version = block.second.major_version;
|
|
for (const auto& blob : blobs)
|
|
{
|
|
cryptonote::transaction tx;
|
|
if (!cryptonote::parse_and_validate_tx_from_blob(blob, tx))
|
|
{
|
|
log::error(logcat, "tx could not be validated from blob, possibly corrupt blockchain");
|
|
continue;
|
|
}
|
|
if (tx.type == cryptonote::txtype::state_change)
|
|
{
|
|
cryptonote::tx_extra_service_node_state_change state_change;
|
|
if (!cryptonote::get_service_node_state_change_from_tx_extra(tx.extra, state_change, hard_fork_version))
|
|
{
|
|
log::error(logcat, "Could not get state change from tx, possibly corrupt tx, hf_version {}", static_cast<int>(hard_fork_version));
|
|
continue;
|
|
}
|
|
|
|
switch(state_change.state) {
|
|
case service_nodes::new_state::deregister:
|
|
total_deregister++;
|
|
break;
|
|
|
|
case service_nodes::new_state::decommission:
|
|
total_decommission++;
|
|
break;
|
|
|
|
case service_nodes::new_state::recommission:
|
|
total_recommission++;
|
|
break;
|
|
|
|
case service_nodes::new_state::ip_change_penalty:
|
|
total_ip_change_penalty++;
|
|
break;
|
|
|
|
default:
|
|
log::error(logcat, "Unhandled state in on_get_service_nodes_state_changes");
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (tx.type == cryptonote::txtype::key_image_unlock)
|
|
{
|
|
total_unlock++;
|
|
}
|
|
}
|
|
}
|
|
|
|
get_sn_state_changes.response["total_deregister"] = total_deregister;
|
|
get_sn_state_changes.response["total_decommission"] = total_decommission;
|
|
get_sn_state_changes.response["total_recommission"] = total_recommission;
|
|
get_sn_state_changes.response["total_ip_change_penalty"] = total_ip_change_penalty;
|
|
get_sn_state_changes.response["total_unlock"] = total_unlock;
|
|
get_sn_state_changes.response["status"] = STATUS_OK;
|
|
}
|
|
//------------------------------------------------------------------------------------------------------------------------------
|
|
void core_rpc_server::invoke(REPORT_PEER_STATUS& report_peer_status, rpc_context context)
|
|
{
|
|
crypto::public_key pubkey;
|
|
if (!tools::hex_to_type(report_peer_status.request.pubkey, pubkey)) {
|
|
log::error(logcat, "Could not parse public key: {}", report_peer_status.request.pubkey);
|
|
throw rpc_error{ERROR_WRONG_PARAM, "Could not parse public key"};
|
|
}
|
|
|
|
bool success = false;
|
|
if (report_peer_status.request.type == "lokinet")
|
|
success = m_core.get_service_node_list().set_lokinet_peer_reachable(pubkey, report_peer_status.request.passed);
|
|
else if (report_peer_status.request.type == "storage" || report_peer_status.request.type == "reachability" /* TODO: old name, can be removed once SS no longer uses it */)
|
|
success = m_core.get_service_node_list().set_storage_server_peer_reachable(pubkey, report_peer_status.request.passed);
|
|
else
|
|
throw rpc_error{ERROR_WRONG_PARAM, "Unknown status type"};
|
|
if (!success)
|
|
throw rpc_error{ERROR_WRONG_PARAM, "Pubkey not found"};
|
|
|
|
report_peer_status.response["status"] = STATUS_OK;
|
|
}
|
|
//------------------------------------------------------------------------------------------------------------------------------
|
|
void core_rpc_server::invoke(TEST_TRIGGER_P2P_RESYNC& test_trigger_p2p_resync, rpc_context context)
|
|
{
|
|
m_p2p.reset_peer_handshake_timer();
|
|
test_trigger_p2p_resync.response["status"] = STATUS_OK;
|
|
}
|
|
//------------------------------------------------------------------------------------------------------------------------------
|
|
void core_rpc_server::invoke(TEST_TRIGGER_UPTIME_PROOF& test_trigger_uptime_proof, rpc_context context)
|
|
{
|
|
if (m_core.get_nettype() != cryptonote::network_type::MAINNET)
|
|
m_core.submit_uptime_proof();
|
|
|
|
test_trigger_uptime_proof.response["status"] = STATUS_OK;
|
|
}
|
|
//------------------------------------------------------------------------------------------------------------------------------
|
|
ONS_NAMES_TO_OWNERS::response core_rpc_server::invoke(ONS_NAMES_TO_OWNERS::request&& req, rpc_context context)
|
|
{
|
|
ONS_NAMES_TO_OWNERS::response res{};
|
|
|
|
if (!context.admin)
|
|
check_quantity_limit(req.entries.size(), ONS_NAMES_TO_OWNERS::MAX_REQUEST_ENTRIES);
|
|
|
|
std::optional<uint64_t> height = m_core.get_current_blockchain_height();
|
|
auto hf_version = get_network_version(nettype(), *height);
|
|
if (req.include_expired) height = std::nullopt;
|
|
|
|
std::vector<ons::mapping_type> types;
|
|
|
|
ons::name_system_db &db = m_core.get_blockchain_storage().name_system_db();
|
|
for (size_t request_index = 0; request_index < req.entries.size(); request_index++)
|
|
{
|
|
ONS_NAMES_TO_OWNERS::request_entry const &request = req.entries[request_index];
|
|
if (!context.admin)
|
|
check_quantity_limit(request.types.size(), ONS_NAMES_TO_OWNERS::MAX_TYPE_REQUEST_ENTRIES, "types");
|
|
|
|
types.clear();
|
|
if (types.capacity() < request.types.size())
|
|
types.reserve(request.types.size());
|
|
for (auto type : request.types)
|
|
{
|
|
types.push_back(static_cast<ons::mapping_type>(type));
|
|
if (!ons::mapping_type_allowed(hf_version, types.back()))
|
|
throw rpc_error{ERROR_WRONG_PARAM, "Invalid lokinet type '" + std::to_string(type) + "'"};
|
|
}
|
|
|
|
// This also takes 32 raw bytes, but that is undocumented (because it is painful to pass
|
|
// through json).
|
|
auto name_hash = ons::name_hash_input_to_base64(request.name_hash);
|
|
if (!name_hash)
|
|
throw rpc_error{ERROR_WRONG_PARAM, "Invalid name_hash: expected hash as 64 hex digits or 43/44 base64 characters"};
|
|
|
|
std::vector<ons::mapping_record> records = db.get_mappings(types, *name_hash, height);
|
|
for (auto const &record : records)
|
|
{
|
|
auto& entry = res.entries.emplace_back();
|
|
entry.entry_index = request_index;
|
|
entry.type = record.type;
|
|
entry.name_hash = record.name_hash;
|
|
entry.owner = record.owner.to_string(nettype());
|
|
if (record.backup_owner) entry.backup_owner = record.backup_owner.to_string(nettype());
|
|
entry.encrypted_value = oxenc::to_hex(record.encrypted_value.to_view());
|
|
entry.expiration_height = record.expiration_height;
|
|
entry.update_height = record.update_height;
|
|
entry.txid = tools::type_to_hex(record.txid);
|
|
}
|
|
}
|
|
|
|
res.status = STATUS_OK;
|
|
return res;
|
|
}
|
|
//------------------------------------------------------------------------------------------------------------------------------
|
|
void core_rpc_server::invoke(ONS_OWNERS_TO_NAMES& ons_owners_to_names, rpc_context context)
|
|
{
|
|
if (!context.admin)
|
|
check_quantity_limit(ons_owners_to_names.request.entries.size(), ONS_OWNERS_TO_NAMES::MAX_REQUEST_ENTRIES);
|
|
|
|
std::unordered_map<ons::generic_owner, size_t> owner_to_request_index;
|
|
std::vector<ons::generic_owner> owners;
|
|
|
|
owners.reserve(ons_owners_to_names.request.entries.size());
|
|
for (size_t request_index = 0; request_index < ons_owners_to_names.request.entries.size(); request_index++)
|
|
{
|
|
std::string const &owner = ons_owners_to_names.request.entries[request_index];
|
|
ons::generic_owner ons_owner = {};
|
|
std::string errmsg;
|
|
if (!ons::parse_owner_to_generic_owner(m_core.get_nettype(), owner, ons_owner, &errmsg))
|
|
throw rpc_error{ERROR_WRONG_PARAM, std::move(errmsg)};
|
|
|
|
// TODO(oxen): We now serialize both owner and backup_owner, since if
|
|
// we specify an owner that is backup owner, we don't show the (other)
|
|
// owner. For RPC compatibility we keep the request_index around until the
|
|
// next hard fork (16)
|
|
owners.push_back(ons_owner);
|
|
owner_to_request_index[ons_owner] = request_index;
|
|
}
|
|
|
|
ons::name_system_db &db = m_core.get_blockchain_storage().name_system_db();
|
|
std::optional<uint64_t> height;
|
|
if (!ons_owners_to_names.request.include_expired) height = m_core.get_current_blockchain_height();
|
|
|
|
std::vector<ONS_OWNERS_TO_NAMES::response_entry> entries;
|
|
std::vector<ons::mapping_record> records = db.get_mappings_by_owners(owners, height);
|
|
for (auto &record : records)
|
|
{
|
|
auto it = owner_to_request_index.end();
|
|
if (record.owner)
|
|
it = owner_to_request_index.find(record.owner);
|
|
if (it == owner_to_request_index.end() && record.backup_owner)
|
|
it = owner_to_request_index.find(record.backup_owner);
|
|
if (it == owner_to_request_index.end())
|
|
throw rpc_error{ERROR_INTERNAL,
|
|
(record.owner ? ("Owner=" + record.owner.to_string(nettype()) + " ") : ""s) +
|
|
(record.backup_owner ? ("BackupOwner=" + record.backup_owner.to_string(nettype()) + " ") : ""s) +
|
|
" could not be mapped back a index in the request 'entries' array"};
|
|
|
|
auto& entry = entries.emplace_back();
|
|
entry.request_index = it->second;
|
|
entry.type = record.type;
|
|
entry.name_hash = std::move(record.name_hash);
|
|
if (record.owner) entry.owner = record.owner.to_string(nettype());
|
|
if (record.backup_owner) entry.backup_owner = record.backup_owner.to_string(nettype());
|
|
entry.encrypted_value = oxenc::to_hex(record.encrypted_value.to_view());
|
|
entry.update_height = record.update_height;
|
|
entry.expiration_height = record.expiration_height;
|
|
entry.txid = tools::type_to_hex(record.txid);
|
|
}
|
|
|
|
ons_owners_to_names.response["entries"] = entries;
|
|
ons_owners_to_names.response["status"] = STATUS_OK;
|
|
return;
|
|
}
|
|
|
|
//------------------------------------------------------------------------------------------------------------------------------
|
|
void core_rpc_server::invoke(ONS_RESOLVE& resolve, rpc_context context)
|
|
{
|
|
auto& req = resolve.request;
|
|
if (req.type < 0 || req.type >= tools::enum_count<ons::mapping_type>)
|
|
throw rpc_error{ERROR_WRONG_PARAM, "Unable to resolve ONS address: 'type' parameter not specified"};
|
|
|
|
auto name_hash = ons::name_hash_input_to_base64(req.name_hash);
|
|
if (!name_hash)
|
|
throw rpc_error{ERROR_WRONG_PARAM, "Unable to resolve ONS address: invalid 'name_hash' value '" + req.name_hash + "'"};
|
|
|
|
|
|
auto hf_version = m_core.get_blockchain_storage().get_network_version();
|
|
auto type = static_cast<ons::mapping_type>(req.type);
|
|
if (!ons::mapping_type_allowed(hf_version, type))
|
|
throw rpc_error{ERROR_WRONG_PARAM, "Invalid lokinet type '" + std::to_string(req.type) + "'"};
|
|
|
|
if (auto mapping = m_core.get_blockchain_storage().name_system_db().resolve(
|
|
type, *name_hash, m_core.get_current_blockchain_height()))
|
|
{
|
|
auto [val, nonce] = mapping->value_nonce(type);
|
|
resolve.response_hex["encrypted_value"] = val;
|
|
if (val.size() < mapping->to_view().size())
|
|
resolve.response_hex["nonce"] = nonce;
|
|
}
|
|
}
|
|
|
|
void core_rpc_server::invoke(GET_ACCRUED_BATCHED_EARNINGS& get_accrued_batched_earnings, rpc_context context)
|
|
{
|
|
auto& blockchain = m_core.get_blockchain_storage();
|
|
bool at_least_one_succeeded = false;
|
|
|
|
auto& balances = get_accrued_batched_earnings.response["balances"];
|
|
auto& req = get_accrued_batched_earnings.request;
|
|
if (req.addresses.size() > 0)
|
|
{
|
|
for (const auto& address : req.addresses)
|
|
{
|
|
uint64_t amount = 0;
|
|
if (cryptonote::is_valid_address(address, nettype()))
|
|
{
|
|
amount = blockchain.sqlite_db()->get_accrued_earnings(address);
|
|
at_least_one_succeeded = true;
|
|
}
|
|
balances[address] = amount;
|
|
}
|
|
} else {
|
|
auto [addresses, amounts] = blockchain.sqlite_db()->get_all_accrued_earnings();
|
|
for (size_t i = 0; i < addresses.size(); i++)
|
|
{
|
|
balances[addresses[i]] = amounts[i];
|
|
}
|
|
at_least_one_succeeded = true;
|
|
}
|
|
|
|
if (!at_least_one_succeeded)
|
|
throw rpc_error{ERROR_WRONG_PARAM, "Failed to query any service nodes batched amounts at all"};
|
|
|
|
get_accrued_batched_earnings.response["status"] = STATUS_OK;
|
|
}
|
|
|
|
} // namespace cryptonote::rpc
|