daemon, wallet: new pay for RPC use system

Daemons intended for public use can be set up to require payment
in the form of hashes in exchange for RPC service. This enables
public daemons to receive payment for their work over a large
number of calls. This system behaves similarly to a pool, so
payment takes the form of valid blocks every so often, yielding
a large one off payment, rather than constant micropayments.

This system can also be used by third parties as a "paywall"
layer, where users of a service can pay for use by mining Monero
to the service provider's address. An example of this for web
site access is Primo, a Monero mining based website "paywall":
https://github.com/selene-kovri/primo

This has some advantages:
 - incentive to run a node providing RPC services, thereby promoting the availability of third party nodes for those who can't run their own
 - incentive to run your own node instead of using a third party's, thereby promoting decentralization
 - decentralized: payment is done between a client and server, with no third party needed
 - private: since the system is "pay as you go", you don't need to identify yourself to claim a long lived balance
 - no payment occurs on the blockchain, so there is no extra transactional load
 - one may mine with a beefy server, and use those credits from a phone, by reusing the client ID (at the cost of some privacy)
 - no barrier to entry: anyone may run a RPC node, and your expected revenue depends on how much work you do
 - Sybil resistant: if you run 1000 idle RPC nodes, you don't magically get more revenue
 - no large credit balance maintained on servers, so they have no incentive to exit scam
 - you can use any/many node(s), since there's little cost in switching servers
 - market based prices: competition between servers to lower costs
 - incentive for a distributed third party node system: if some public nodes are overused/slow, traffic can move to others
 - increases network security
 - helps counteract mining pools' share of the network hash rate
 - zero incentive for a payer to "double spend" since a reorg does not give any money back to the miner

And some disadvantages:
 - low power clients will have difficulty mining (but one can optionally mine in advance and/or with a faster machine)
 - payment is "random", so a server might go a long time without a block before getting one
 - a public node's overall expected payment may be small

Public nodes are expected to compete to find a suitable level for
cost of service.

The daemon can be set up this way to require payment for RPC services:

  monerod --rpc-payment-address 4xxxxxx \
    --rpc-payment-credits 250 --rpc-payment-difficulty 1000

These values are an example only.

The --rpc-payment-difficulty switch selects how hard each "share" should
be, similar to a mining pool. The higher the difficulty, the fewer
shares a client will find.
The --rpc-payment-credits switch selects how many credits are awarded
for each share a client finds.
Considering both options, clients will be awarded credits/difficulty
credits for every hash they calculate. For example, in the command line
above, 0.25 credits per hash. A client mining at 100 H/s will therefore
get an average of 25 credits per second.
For reference, in the current implementation, a credit is enough to
sync 20 blocks, so a 100 H/s client that's just starting to use Monero
and uses this daemon will be able to sync 500 blocks per second.

The wallet can be set to automatically mine if connected to a daemon
which requires payment for RPC usage. It will try to keep a balance
of 50000 credits, stopping mining when it's at this level, and starting
again as credits are spent. With the example above, a new client will
mine this much credits in about half an hour, and this target is enough
to sync 500000 blocks (currently about a third of the monero blockchain).

There are three new settings in the wallet:

 - credits-target: this is the amount of credits a wallet will try to
reach before stopping mining. The default of 0 means 50000 credits.

 - auto-mine-for-rpc-payment-threshold: this controls the minimum
credit rate which the wallet considers worth mining for. If the
daemon credits less than this ratio, the wallet will consider mining
to be not worth it. In the example above, the rate is 0.25

 - persistent-rpc-client-id: if set, this allows the wallet to reuse
a client id across runs. This means a public node can tell a wallet
that's connecting is the same as one that connected previously, but
allows a wallet to keep their credit balance from one run to the
other. Since the wallet only mines to keep a small credit balance,
this is not normally worth doing. However, someone may want to mine
on a fast server, and use that credit balance on a low power device
such as a phone. If left unset, a new client ID is generated at
each wallet start, for privacy reasons.

To mine and use a credit balance on two different devices, you can
use the --rpc-client-secret-key switch. A wallet's client secret key
can be found using the new rpc_payments command in the wallet.
Note: anyone knowing your RPC client secret key is able to use your
credit balance.

The wallet has a few new commands too:

 - start_mining_for_rpc: start mining to acquire more credits,
regardless of the auto mining settings
 - stop_mining_for_rpc: stop mining to acquire more credits
 - rpc_payments: display information about current credits with
the currently selected daemon

The node has an extra command:

 - rpc_payments: display information about clients and their
balances

The node will forget about any balance for clients which have
been inactive for 6 months. Balances carry over on node restart.
This commit is contained in:
moneromooo-monero 2018-02-11 15:15:56 +00:00
parent b3a9a4d99d
commit 2899379791
No known key found for this signature in database
GPG key ID: 686F07454D6CEFC3
54 changed files with 4140 additions and 836 deletions

View file

@ -132,6 +132,23 @@ static inline uint32_t div128_32(uint64_t dividend_hi, uint64_t dividend_lo, uin
// Long divisor with 2^64 base
void div128_64(uint64_t dividend_hi, uint64_t dividend_lo, uint64_t divisor, uint64_t* quotient_hi, uint64_t *quotient_lo, uint64_t *remainder_hi, uint64_t *remainder_lo);
static inline void add64clamp(uint64_t *value, uint64_t add)
{
static const uint64_t maxval = (uint64_t)-1;
if (*value > maxval - add)
*value = maxval;
else
*value += add;
}
static inline void sub64clamp(uint64_t *value, uint64_t sub)
{
if (*value < sub)
*value = 0;
else
*value -= sub;
}
#define IDENT16(x) ((uint16_t) (x))
#define IDENT32(x) ((uint32_t) (x))
#define IDENT64(x) ((uint64_t) (x))

View file

@ -259,6 +259,11 @@ namespace math_helper
m_last_worked_time = get_time();
}
void trigger()
{
m_last_worked_time = 0;
}
template<class functor_t>
bool do_call(functor_t functr)
{

View file

@ -26,6 +26,7 @@
#pragma once
#include <type_traits>
#include <boost/utility/value_init.hpp>
#include <boost/foreach.hpp>
#include "misc_log_ex.h"
@ -45,18 +46,20 @@ public: \
template<class t_storage> \
bool store( t_storage& st, typename t_storage::hsection hparent_section = nullptr) const\
{\
return serialize_map<true>(*this, st, hparent_section);\
using type = typename std::remove_const<typename std::remove_reference<decltype(*this)>::type>::type; \
auto &self = const_cast<type&>(*this); \
return self.template serialize_map<true>(st, hparent_section); \
}\
template<class t_storage> \
bool _load( t_storage& stg, typename t_storage::hsection hparent_section = nullptr)\
{\
return serialize_map<false>(*this, stg, hparent_section);\
return serialize_map<false>(stg, hparent_section);\
}\
template<class t_storage> \
bool load( t_storage& stg, typename t_storage::hsection hparent_section = nullptr)\
{\
try{\
return serialize_map<false>(*this, stg, hparent_section);\
return serialize_map<false>(stg, hparent_section);\
}\
catch(const std::exception& err) \
{ \
@ -65,13 +68,22 @@ public: \
return false; \
}\
}\
template<bool is_store, class this_type, class t_storage> \
static bool serialize_map(this_type& this_ref, t_storage& stg, typename t_storage::hsection hparent_section) \
{
/*template<typename T> T& this_type_resolver() { return *this; }*/ \
/*using this_type = std::result_of<decltype(this_type_resolver)>::type;*/ \
template<bool is_store, class t_storage> \
bool serialize_map(t_storage& stg, typename t_storage::hsection hparent_section) \
{ \
decltype(*this) &this_ref = *this;
#define KV_SERIALIZE_N(varialble, val_name) \
epee::serialization::selector<is_store>::serialize(this_ref.varialble, stg, hparent_section, val_name);
#define KV_SERIALIZE_PARENT(type) \
do { \
if (!((type*)this)->serialize_map<is_store, t_storage>(stg, hparent_section)) \
return false; \
} while(0);
template<typename T> inline void serialize_default(const T &t, T v) { }
template<typename T> inline void serialize_default(T &t, T v) { t = v; }

View file

@ -101,7 +101,7 @@ namespace epee
}
template<class t_request, class t_response, class t_transport>
bool invoke_http_json_rpc(const boost::string_ref uri, std::string method_name, const t_request& out_struct, t_response& result_struct, t_transport& transport, std::chrono::milliseconds timeout = std::chrono::seconds(15), const boost::string_ref http_method = "GET", const std::string& req_id = "0")
bool invoke_http_json_rpc(const boost::string_ref uri, std::string method_name, const t_request& out_struct, t_response& result_struct, epee::json_rpc::error &error_struct, t_transport& transport, std::chrono::milliseconds timeout = std::chrono::seconds(15), const boost::string_ref http_method = "GET", const std::string& req_id = "0")
{
epee::json_rpc::request<t_request> req_t = AUTO_VAL_INIT(req_t);
req_t.jsonrpc = "2.0";
@ -111,10 +111,12 @@ namespace epee
epee::json_rpc::response<t_response, epee::json_rpc::error> resp_t = AUTO_VAL_INIT(resp_t);
if(!epee::net_utils::invoke_http_json(uri, req_t, resp_t, transport, timeout, http_method))
{
error_struct = {};
return false;
}
if(resp_t.error.code || resp_t.error.message.size())
{
error_struct = resp_t.error;
LOG_ERROR("RPC call of \"" << req_t.method << "\" returned error: " << resp_t.error.code << ", message: " << resp_t.error.message);
return false;
}
@ -122,6 +124,13 @@ namespace epee
return true;
}
template<class t_request, class t_response, class t_transport>
bool invoke_http_json_rpc(const boost::string_ref uri, std::string method_name, const t_request& out_struct, t_response& result_struct, t_transport& transport, std::chrono::milliseconds timeout = std::chrono::seconds(15), const boost::string_ref http_method = "GET", const std::string& req_id = "0")
{
epee::json_rpc::error error_struct;
return invoke_http_json_rpc(uri, method_name, out_struct, result_struct, error_struct, transport, timeout, http_method, req_id);
}
template<class t_command, class t_transport>
bool invoke_http_json_rpc(const boost::string_ref uri, typename t_command::request& out_struct, typename t_command::response& result_struct, t_transport& transport, std::chrono::milliseconds timeout = std::chrono::seconds(15), const boost::string_ref http_method = "GET", const std::string& req_id = "0")
{

View file

@ -100,7 +100,7 @@ static const char *get_default_categories(int level)
switch (level)
{
case 0:
categories = "*:WARNING,net:FATAL,net.http:FATAL,net.ssl:FATAL,net.p2p:FATAL,net.cn:FATAL,global:INFO,verify:FATAL,serialization:FATAL,stacktrace:INFO,logging:INFO,msgwriter:INFO";
categories = "*:WARNING,net:FATAL,net.http:FATAL,net.ssl:FATAL,net.p2p:FATAL,net.cn:FATAL,global:INFO,verify:FATAL,serialization:FATAL,daemon.rpc.payment:ERROR,stacktrace:INFO,logging:INFO,msgwriter:INFO";
break;
case 1:
categories = "*:INFO,global:INFO,stacktrace:INFO,logging:INFO,msgwriter:INFO,perf.*:DEBUG";

View file

@ -76,14 +76,15 @@ private:
void set_performance_timer_log_level(el::Level level);
#define PERF_TIMER_UNIT(name, unit) tools::LoggingPerformanceTimer pt_##name(#name, "perf." MONERO_DEFAULT_LOG_CATEGORY, unit, tools::performance_timer_log_level)
#define PERF_TIMER_UNIT_L(name, unit, l) tools::LoggingPerformanceTimer pt_##name(#name, "perf." MONERO_DEFAULT_LOG_CATEGORY, unit, l)
#define PERF_TIMER_NAME(name) pt_##name
#define PERF_TIMER_UNIT(name, unit) tools::LoggingPerformanceTimer PERF_TIMER_NAME(name)(#name, "perf." MONERO_DEFAULT_LOG_CATEGORY, unit, tools::performance_timer_log_level)
#define PERF_TIMER_UNIT_L(name, unit, l) tools::LoggingPerformanceTimer PERF_TIMER_NAME(name)t_##name(#name, "perf." MONERO_DEFAULT_LOG_CATEGORY, unit, l)
#define PERF_TIMER(name) PERF_TIMER_UNIT(name, 1000000)
#define PERF_TIMER_L(name, l) PERF_TIMER_UNIT_L(name, 1000000, l)
#define PERF_TIMER_START_UNIT(name, unit) std::unique_ptr<tools::LoggingPerformanceTimer> pt_##name(new tools::LoggingPerformanceTimer(#name, "perf." MONERO_DEFAULT_LOG_CATEGORY, unit, el::Level::Info))
#define PERF_TIMER_START_UNIT(name, unit) std::unique_ptr<tools::LoggingPerformanceTimer> PERF_TIMER_NAME(name)(new tools::LoggingPerformanceTimer(#name, "perf." MONERO_DEFAULT_LOG_CATEGORY, unit, el::Level::Info))
#define PERF_TIMER_START(name) PERF_TIMER_START_UNIT(name, 1000000)
#define PERF_TIMER_STOP(name) do { pt_##name.reset(NULL); } while(0)
#define PERF_TIMER_PAUSE(name) pt_##name->pause()
#define PERF_TIMER_RESUME(name) pt_##name->resume()
#define PERF_TIMER_STOP(name) do { PERF_TIMER_NAME(name).reset(NULL); } while(0)
#define PERF_TIMER_PAUSE(name) PERF_TIMER_NAME(name)->pause()
#define PERF_TIMER_RESUME(name) PERF_TIMER_NAME(name)->resume()
}

View file

@ -43,7 +43,7 @@ namespace cryptonote
{
cryptonote_connection_context(): m_state(state_before_handshake), m_remote_blockchain_height(0), m_last_response_height(0),
m_last_request_time(boost::date_time::not_a_date_time), m_callback_request_count(0),
m_last_known_hash(crypto::null_hash), m_pruning_seed(0), m_rpc_port(0), m_anchor(false) {}
m_last_known_hash(crypto::null_hash), m_pruning_seed(0), m_rpc_port(0), m_rpc_credits_per_hash(0), m_anchor(false) {}
enum state
{
@ -64,6 +64,7 @@ namespace cryptonote
crypto::hash m_last_known_hash;
uint32_t m_pruning_seed;
uint16_t m_rpc_port;
uint32_t m_rpc_credits_per_hash;
bool m_anchor;
//size_t m_score; TODO: add score calculations
};

View file

@ -148,6 +148,7 @@
#define CRYPTONOTE_BLOCKCHAINDATA_FILENAME "data.mdb"
#define CRYPTONOTE_BLOCKCHAINDATA_LOCK_FILENAME "lock.mdb"
#define P2P_NET_DATA_FILENAME "p2pstate.bin"
#define RPC_PAYMENTS_DATA_FILENAME "rpcpayments.bin"
#define MINER_CONFIG_FILE_NAME "miner_conf.json"
#define THREAD_STACK_SIZE 5 * 1024 * 1024
@ -180,6 +181,8 @@
#define CRYPTONOTE_PRUNING_TIP_BLOCKS 5500 // the smaller, the more space saved
//#define CRYPTONOTE_PRUNING_DEBUG_SPOOF_SEED
#define RPC_CREDITS_PER_HASH_SCALE ((float)(1<<24))
// New constants are intended to go here
namespace config
{

View file

@ -56,6 +56,7 @@ namespace cryptonote
std::string ip;
std::string port;
uint16_t rpc_port;
uint32_t rpc_credits_per_hash;
std::string peer_id;
@ -94,6 +95,7 @@ namespace cryptonote
KV_SERIALIZE(ip)
KV_SERIALIZE(port)
KV_SERIALIZE(rpc_port)
KV_SERIALIZE(rpc_credits_per_hash)
KV_SERIALIZE(peer_id)
KV_SERIALIZE(recv_count)
KV_SERIALIZE(recv_idle_time)

View file

@ -246,6 +246,7 @@ namespace cryptonote
cnx.port = std::to_string(cntxt.m_remote_address.as<epee::net_utils::ipv4_network_address>().port());
}
cnx.rpc_port = cntxt.m_rpc_port;
cnx.rpc_credits_per_hash = cntxt.m_rpc_credits_per_hash;
std::stringstream peer_id_str;
peer_id_str << std::hex << std::setw(16) << peer_id;

View file

@ -794,6 +794,13 @@ bool t_command_parser_executor::pop_blocks(const std::vector<std::string>& args)
return false;
}
bool t_command_parser_executor::rpc_payments(const std::vector<std::string>& args)
{
if (args.size() != 0) return false;
return m_executor.rpc_payments();
}
bool t_command_parser_executor::version(const std::vector<std::string>& args)
{
std::cout << "Monero '" << MONERO_RELEASE_NAME << "' (v" << MONERO_VERSION_FULL << ")" << std::endl;

View file

@ -143,6 +143,8 @@ public:
bool pop_blocks(const std::vector<std::string>& args);
bool rpc_payments(const std::vector<std::string>& args);
bool version(const std::vector<std::string>& args);
bool prune_blockchain(const std::vector<std::string>& args);

View file

@ -295,6 +295,11 @@ t_command_server::t_command_server(
, "pop_blocks <nblocks>"
, "Remove blocks from end of blockchain"
);
m_command_lookup.set_handler(
"rpc_payments"
, std::bind(&t_command_parser_executor::rpc_payments, &m_parser, p::_1)
, "Print information about RPC payments."
);
m_command_lookup.set_handler(
"version"
, std::bind(&t_command_parser_executor::version, &m_parser, p::_1)

View file

@ -59,6 +59,18 @@ public:
: m_core{nullptr}
, m_vm_HACK{vm}
{
//initialize core here
MGINFO("Initializing core...");
#if defined(PER_BLOCK_CHECKPOINT)
const cryptonote::GetCheckpointsCallback& get_checkpoints = blocks::GetCheckpointsData;
#else
const cryptonote::GetCheckpointsCallback& get_checkpoints = nullptr;
#endif
if (!m_core.init(m_vm_HACK, nullptr, get_checkpoints))
{
throw std::runtime_error("Failed to initialize core");
}
MGINFO("Core initialized OK");
}
// TODO - get rid of circular dependencies in internals
@ -69,18 +81,6 @@ public:
bool run()
{
//initialize core here
MGINFO("Initializing core...");
#if defined(PER_BLOCK_CHECKPOINT)
const cryptonote::GetCheckpointsCallback& get_checkpoints = blocks::GetCheckpointsData;
#else
const cryptonote::GetCheckpointsCallback& get_checkpoints = nullptr;
#endif
if (!m_core.init(m_vm_HACK, nullptr, get_checkpoints))
{
return false;
}
MGINFO("Core initialized OK");
return true;
}

View file

@ -37,6 +37,7 @@
#include "cryptonote_core/cryptonote_core.h"
#include "cryptonote_basic/difficulty.h"
#include "cryptonote_basic/hardfork.h"
#include "rpc/rpc_payment_signature.h"
#include <boost/format.hpp>
#include <ctime>
#include <string>
@ -60,6 +61,13 @@ namespace {
}
}
std::string print_float(float f, int prec)
{
char buf[16];
snprintf(buf, sizeof(buf), "%*.*f", prec, prec, f);
return buf;
}
void print_peer(std::string const & prefix, cryptonote::peer const & peer, bool pruned_only, bool publicrpc_only)
{
if (pruned_only && peer.pruning_seed == 0)
@ -77,8 +85,9 @@ namespace {
epee::string_tools::xtype_to_string(peer.port, port_str);
std::string addr_str = peer.host + ":" + port_str;
std::string rpc_port = peer.rpc_port ? std::to_string(peer.rpc_port) : "-";
std::string rpc_credits_per_hash = peer.rpc_credits_per_hash ? print_float(peer.rpc_credits_per_hash / RPC_CREDITS_PER_HASH_SCALE, 2) : "-";
std::string pruning_seed = epee::string_tools::to_string_hex(peer.pruning_seed);
tools::msg_writer() << boost::format("%-10s %-25s %-25s %-5s %-4s %s") % prefix % id_str % addr_str % rpc_port % pruning_seed % elapsed;
tools::msg_writer() << boost::format("%-10s %-25s %-25s %-5s %-5s %-4s %s") % prefix % id_str % addr_str % rpc_port % rpc_credits_per_hash % pruning_seed % elapsed;
}
void print_block_header(cryptonote::block_header_response const & header)
@ -2362,4 +2371,45 @@ bool t_rpc_command_executor::set_bootstrap_daemon(
return true;
}
bool t_rpc_command_executor::rpc_payments()
{
cryptonote::COMMAND_RPC_ACCESS_DATA::request req;
cryptonote::COMMAND_RPC_ACCESS_DATA::response res;
std::string fail_message = "Unsuccessful";
epee::json_rpc::error error_resp;
if (m_is_rpc)
{
if (!m_rpc_client->json_rpc_request(req, res, "rpc_access_data", fail_message.c_str()))
{
return true;
}
}
else
{
if (!m_rpc_server->on_rpc_access_data(req, res, error_resp) || res.status != CORE_RPC_STATUS_OK)
{
tools::fail_msg_writer() << make_error(fail_message, res.status);
return true;
}
}
const uint64_t now = std::chrono::duration_cast<std::chrono::seconds>(std::chrono::system_clock::now().time_since_epoch()).count();
uint64_t balance = 0;
tools::msg_writer() << boost::format("%64s %16u %16u %8u %8u %8u %8u %s")
% "Client ID" % "Balance" % "Total mined" % "Good" % "Stale" % "Bad" % "Dupes" % "Last update";
for (const auto &entry: res.entries)
{
tools::msg_writer() << boost::format("%64s %16u %16u %8u %8u %8u %8u %s")
% entry.client % entry.balance % entry.credits_total
% entry.nonces_good % entry.nonces_stale % entry.nonces_bad % entry.nonces_dupe
% (entry.last_update_time == 0 ? "never" : get_human_time_ago(entry.last_update_time, now).c_str());
balance += entry.balance;
}
tools::msg_writer() << res.entries.size() << " clients with a total of " << balance << " credits";
tools::msg_writer() << "Aggregated client hash rate: " << get_mining_speed(res.hashrate);
return true;
}
}// namespace daemonize

View file

@ -167,6 +167,8 @@ public:
const std::string &address,
const std::string &username,
const std::string &password);
bool rpc_payments();
};
} // namespace daemonize

View file

@ -231,6 +231,7 @@ namespace nodetool
: m_payload_handler(payload_handler),
m_external_port(0),
m_rpc_port(0),
m_rpc_credits_per_hash(0),
m_allow_local_ip(false),
m_hide_my_port(false),
m_igd(no_igd),
@ -431,6 +432,11 @@ namespace nodetool
m_rpc_port = rpc_port;
}
void set_rpc_credits_per_hash(uint32_t rpc_credits_per_hash)
{
m_rpc_credits_per_hash = rpc_credits_per_hash;
}
private:
std::string m_config_folder;
@ -440,6 +446,7 @@ namespace nodetool
uint32_t m_listening_port_ipv6;
uint32_t m_external_port;
uint16_t m_rpc_port;
uint32_t m_rpc_credits_per_hash;
bool m_allow_local_ip;
bool m_hide_my_port;
igd_t m_igd;

View file

@ -1057,7 +1057,8 @@ namespace nodetool
pi = context.peer_id = rsp.node_data.peer_id;
context.m_rpc_port = rsp.node_data.rpc_port;
m_network_zones.at(context.m_remote_address.get_zone()).m_peerlist.set_peer_just_seen(rsp.node_data.peer_id, context.m_remote_address, context.m_pruning_seed, context.m_rpc_port);
context.m_rpc_credits_per_hash = rsp.node_data.rpc_credits_per_hash;
m_network_zones.at(context.m_remote_address.get_zone()).m_peerlist.set_peer_just_seen(rsp.node_data.peer_id, context.m_remote_address, context.m_pruning_seed, context.m_rpc_port, context.m_rpc_credits_per_hash);
// move
for (auto const& zone : m_network_zones)
@ -1123,7 +1124,7 @@ namespace nodetool
add_host_fail(context.m_remote_address);
}
if(!context.m_is_income)
m_network_zones.at(context.m_remote_address.get_zone()).m_peerlist.set_peer_just_seen(context.peer_id, context.m_remote_address, context.m_pruning_seed, context.m_rpc_port);
m_network_zones.at(context.m_remote_address.get_zone()).m_peerlist.set_peer_just_seen(context.peer_id, context.m_remote_address, context.m_pruning_seed, context.m_rpc_port, context.m_rpc_credits_per_hash);
if (!m_payload_handler.process_payload_sync_data(rsp.payload_data, context, false))
{
m_network_zones.at(context.m_remote_address.get_zone()).m_net_server.get_config_object().close(context.m_connection_id );
@ -1292,6 +1293,7 @@ namespace nodetool
pe_local.last_seen = static_cast<int64_t>(last_seen);
pe_local.pruning_seed = con->m_pruning_seed;
pe_local.rpc_port = con->m_rpc_port;
pe_local.rpc_credits_per_hash = con->m_rpc_credits_per_hash;
zone.m_peerlist.append_with_peer_white(pe_local);
//update last seen and push it to peerlist manager
@ -1922,6 +1924,7 @@ namespace nodetool
else
node_data.my_port = 0;
node_data.rpc_port = zone.m_can_pingback ? m_rpc_port : 0;
node_data.rpc_credits_per_hash = zone.m_can_pingback ? m_rpc_credits_per_hash : 0;
node_data.network_id = m_network_id;
return true;
}
@ -2366,6 +2369,7 @@ namespace nodetool
context.peer_id = arg.node_data.peer_id;
context.m_in_timedsync = false;
context.m_rpc_port = arg.node_data.rpc_port;
context.m_rpc_credits_per_hash = arg.node_data.rpc_credits_per_hash;
if(arg.node_data.my_port && zone.m_can_pingback)
{
@ -2393,6 +2397,7 @@ namespace nodetool
pe.id = peer_id_l;
pe.pruning_seed = context.m_pruning_seed;
pe.rpc_port = context.m_rpc_port;
pe.rpc_credits_per_hash = context.m_rpc_credits_per_hash;
this->m_network_zones.at(context.m_remote_address.get_zone()).m_peerlist.append_with_peer_white(pe);
LOG_DEBUG_CC(context, "PING SUCCESS " << context.m_remote_address.host_str() << ":" << port_l);
});
@ -2710,7 +2715,7 @@ namespace nodetool
}
else
{
zone.second.m_peerlist.set_peer_just_seen(pe.id, pe.adr, pe.pruning_seed, pe.rpc_port);
zone.second.m_peerlist.set_peer_just_seen(pe.id, pe.adr, pe.pruning_seed, pe.rpc_port, pe.rpc_credits_per_hash);
LOG_PRINT_L2("PEER PROMOTED TO WHITE PEER LIST IP address: " << pe.adr.host_str() << " Peer ID: " << peerid_type(pe.id));
}
}

View file

@ -111,7 +111,7 @@ namespace nodetool
bool append_with_peer_white(const peerlist_entry& pr);
bool append_with_peer_gray(const peerlist_entry& pr);
bool append_with_peer_anchor(const anchor_peerlist_entry& ple);
bool set_peer_just_seen(peerid_type peer, const epee::net_utils::network_address& addr, uint32_t pruning_seed, uint16_t rpc_port);
bool set_peer_just_seen(peerid_type peer, const epee::net_utils::network_address& addr, uint32_t pruning_seed, uint16_t rpc_port, uint32_t rpc_credits_per_hash);
bool set_peer_unreachable(const peerlist_entry& pr);
bool is_host_allowed(const epee::net_utils::network_address &address);
bool get_random_gray_peer(peerlist_entry& pe);
@ -315,7 +315,7 @@ namespace nodetool
}
//--------------------------------------------------------------------------------------------------
inline
bool peerlist_manager::set_peer_just_seen(peerid_type peer, const epee::net_utils::network_address& addr, uint32_t pruning_seed, uint16_t rpc_port)
bool peerlist_manager::set_peer_just_seen(peerid_type peer, const epee::net_utils::network_address& addr, uint32_t pruning_seed, uint16_t rpc_port, uint32_t rpc_credits_per_hash)
{
TRY_ENTRY();
CRITICAL_REGION_LOCAL(m_peerlist_lock);
@ -326,6 +326,7 @@ namespace nodetool
ple.last_seen = time(NULL);
ple.pruning_seed = pruning_seed;
ple.rpc_port = rpc_port;
ple.rpc_credits_per_hash = rpc_credits_per_hash;
return append_with_peer_white(ple);
CATCH_ENTRY_L0("peerlist_manager::set_peer_just_seen()", false);
}

View file

@ -42,7 +42,7 @@
#include "common/pruning.h"
#endif
BOOST_CLASS_VERSION(nodetool::peerlist_entry, 2)
BOOST_CLASS_VERSION(nodetool::peerlist_entry, 3)
namespace boost
{
@ -241,6 +241,13 @@ namespace boost
return;
}
a & pl.rpc_port;
if (ver < 3)
{
if (!typename Archive::is_saving())
pl.rpc_credits_per_hash = 0;
return;
}
a & pl.rpc_credits_per_hash;
}
template <class Archive, class ver_type>

View file

@ -77,6 +77,7 @@ namespace nodetool
int64_t last_seen;
uint32_t pruning_seed;
uint16_t rpc_port;
uint32_t rpc_credits_per_hash;
BEGIN_KV_SERIALIZE_MAP()
KV_SERIALIZE(adr)
@ -85,6 +86,7 @@ namespace nodetool
KV_SERIALIZE_OPT(last_seen, (int64_t)0)
KV_SERIALIZE_OPT(pruning_seed, (uint32_t)0)
KV_SERIALIZE_OPT(rpc_port, (uint16_t)0)
KV_SERIALIZE_OPT(rpc_credits_per_hash, (uint32_t)0)
END_KV_SERIALIZE_MAP()
};
typedef peerlist_entry_base<epee::net_utils::network_address> peerlist_entry;
@ -132,6 +134,7 @@ namespace nodetool
{
ss << pe.id << "\t" << pe.adr.str()
<< " \trpc port " << (pe.rpc_port > 0 ? std::to_string(pe.rpc_port) : "-")
<< " \trpc credits per hash " << (pe.rpc_credits_per_hash > 0 ? std::to_string(pe.rpc_credits_per_hash) : "-")
<< " \tpruning seed " << pe.pruning_seed
<< " \tlast_seen: " << (pe.last_seen == 0 ? std::string("never") : epee::misc_utils::get_time_interval_string(now_time - pe.last_seen))
<< std::endl;
@ -166,6 +169,7 @@ namespace nodetool
uint64_t local_time;
uint32_t my_port;
uint16_t rpc_port;
uint32_t rpc_credits_per_hash;
peerid_type peer_id;
BEGIN_KV_SERIALIZE_MAP()
@ -174,6 +178,7 @@ namespace nodetool
KV_SERIALIZE(local_time)
KV_SERIALIZE(my_port)
KV_SERIALIZE_OPT(rpc_port, (uint16_t)(0))
KV_SERIALIZE_OPT(rpc_credits_per_hash, (uint32_t)0)
END_KV_SERIALIZE_MAP()
};
@ -220,7 +225,7 @@ namespace nodetool
{
const epee::net_utils::network_address &na = p.adr;
const epee::net_utils::ipv4_network_address &ipv4 = na.as<const epee::net_utils::ipv4_network_address>();
local_peerlist.push_back(peerlist_entry_base<network_address_old>({{ipv4.ip(), ipv4.port()}, p.id, p.last_seen, p.pruning_seed, p.rpc_port}));
local_peerlist.push_back(peerlist_entry_base<network_address_old>({{ipv4.ip(), ipv4.port()}, p.id, p.last_seen, p.pruning_seed, p.rpc_port, p.rpc_credits_per_hash}));
}
else
MDEBUG("Not including in legacy peer list: " << p.adr.str());
@ -235,7 +240,7 @@ namespace nodetool
std::vector<peerlist_entry_base<network_address_old>> local_peerlist;
epee::serialization::selector<is_store>::serialize_stl_container_pod_val_as_blob(local_peerlist, stg, hparent_section, "local_peerlist");
for (const auto &p: local_peerlist)
((response&)this_ref).local_peerlist_new.push_back(peerlist_entry({epee::net_utils::ipv4_network_address(p.adr.ip, p.adr.port), p.id, p.last_seen, p.pruning_seed, p.rpc_port}));
((response&)this_ref).local_peerlist_new.push_back(peerlist_entry({epee::net_utils::ipv4_network_address(p.adr.ip, p.adr.port), p.id, p.last_seen, p.pruning_seed, p.rpc_port, p.rpc_credits_per_hash}));
}
}
END_KV_SERIALIZE_MAP()

View file

@ -29,12 +29,14 @@
include_directories(SYSTEM ${ZMQ_INCLUDE_PATH})
set(rpc_base_sources
rpc_args.cpp)
rpc_args.cpp
rpc_payment_signature.cpp
rpc_handler.cpp)
set(rpc_sources
bootstrap_daemon.cpp
core_rpc_server.cpp
rpc_handler.cpp
rpc_payment.cpp
instanciations)
set(daemon_messages_sources
@ -47,7 +49,9 @@ set(daemon_rpc_server_sources
set(rpc_base_headers
rpc_args.h)
rpc_args.h
rpc_payment_signature.h
rpc_handler.h)
set(rpc_headers
rpc_handler.h)
@ -58,6 +62,7 @@ set(daemon_rpc_server_headers)
set(rpc_daemon_private_headers
bootstrap_daemon.h
core_rpc_server.h
rpc_payment.h
core_rpc_server_commands_defs.h
core_rpc_server_error_codes.h)

File diff suppressed because it is too large Load diff

View file

@ -42,6 +42,7 @@
#include "cryptonote_core/cryptonote_core.h"
#include "p2p/net_node.h"
#include "cryptonote_protocol/cryptonote_protocol_handler.h"
#include "rpc_payment.h"
#undef MONERO_DEFAULT_LOG_CATEGORY
#define MONERO_DEFAULT_LOG_CATEGORY "daemon.rpc"
@ -71,6 +72,9 @@ namespace cryptonote
static const command_line::arg_descriptor<bool> arg_rpc_ssl_allow_any_cert;
static const command_line::arg_descriptor<std::string> arg_bootstrap_daemon_address;
static const command_line::arg_descriptor<std::string> arg_bootstrap_daemon_login;
static const command_line::arg_descriptor<std::string> arg_rpc_payment_address;
static const command_line::arg_descriptor<uint64_t> arg_rpc_payment_difficulty;
static const command_line::arg_descriptor<uint64_t> arg_rpc_payment_credits;
typedef epee::net_utils::connection_context_base connection_context;
@ -78,6 +82,7 @@ namespace cryptonote
core& cr
, nodetool::node_server<cryptonote::t_cryptonote_protocol_handler<cryptonote::core> >& p2p
);
~core_rpc_server();
static void init_options(boost::program_options::options_description& desc);
bool init(
@ -169,6 +174,12 @@ namespace cryptonote
MAP_JON_RPC_WE("get_txpool_backlog", on_get_txpool_backlog, COMMAND_RPC_GET_TRANSACTION_POOL_BACKLOG)
MAP_JON_RPC_WE("get_output_distribution", on_get_output_distribution, COMMAND_RPC_GET_OUTPUT_DISTRIBUTION)
MAP_JON_RPC_WE_IF("prune_blockchain", on_prune_blockchain, COMMAND_RPC_PRUNE_BLOCKCHAIN, !m_restricted)
MAP_JON_RPC_WE("rpc_access_info", on_rpc_access_info, COMMAND_RPC_ACCESS_INFO)
MAP_JON_RPC_WE("rpc_access_submit_nonce",on_rpc_access_submit_nonce, COMMAND_RPC_ACCESS_SUBMIT_NONCE)
MAP_JON_RPC_WE("rpc_access_pay", on_rpc_access_pay, COMMAND_RPC_ACCESS_PAY)
MAP_JON_RPC_WE_IF("rpc_access_tracking", on_rpc_access_tracking, COMMAND_RPC_ACCESS_TRACKING, !m_restricted)
MAP_JON_RPC_WE_IF("rpc_access_data", on_rpc_access_data, COMMAND_RPC_ACCESS_DATA, !m_restricted)
MAP_JON_RPC_WE_IF("rpc_access_account", on_rpc_access_account, COMMAND_RPC_ACCESS_ACCOUNT, !m_restricted)
END_JSON_RPC_MAP()
END_URI_MAP2()
@ -236,6 +247,12 @@ namespace cryptonote
bool on_get_txpool_backlog(const COMMAND_RPC_GET_TRANSACTION_POOL_BACKLOG::request& req, COMMAND_RPC_GET_TRANSACTION_POOL_BACKLOG::response& res, epee::json_rpc::error& error_resp, const connection_context *ctx = NULL);
bool on_get_output_distribution(const COMMAND_RPC_GET_OUTPUT_DISTRIBUTION::request& req, COMMAND_RPC_GET_OUTPUT_DISTRIBUTION::response& res, epee::json_rpc::error& error_resp, const connection_context *ctx = NULL);
bool on_prune_blockchain(const COMMAND_RPC_PRUNE_BLOCKCHAIN::request& req, COMMAND_RPC_PRUNE_BLOCKCHAIN::response& res, epee::json_rpc::error& error_resp, const connection_context *ctx = NULL);
bool on_rpc_access_info(const COMMAND_RPC_ACCESS_INFO::request& req, COMMAND_RPC_ACCESS_INFO::response& res, epee::json_rpc::error& error_resp, const connection_context *ctx = NULL);
bool on_rpc_access_submit_nonce(const COMMAND_RPC_ACCESS_SUBMIT_NONCE::request& req, COMMAND_RPC_ACCESS_SUBMIT_NONCE::response& res, epee::json_rpc::error& error_resp, const connection_context *ctx = NULL);
bool on_rpc_access_pay(const COMMAND_RPC_ACCESS_PAY::request& req, COMMAND_RPC_ACCESS_PAY::response& res, epee::json_rpc::error& error_resp, const connection_context *ctx = NULL);
bool on_rpc_access_tracking(const COMMAND_RPC_ACCESS_TRACKING::request& req, COMMAND_RPC_ACCESS_TRACKING::response& res, epee::json_rpc::error& error_resp, const connection_context *ctx = NULL);
bool on_rpc_access_data(const COMMAND_RPC_ACCESS_DATA::request& req, COMMAND_RPC_ACCESS_DATA::response& res, epee::json_rpc::error& error_resp, const connection_context *ctx = NULL);
bool on_rpc_access_account(const COMMAND_RPC_ACCESS_ACCOUNT::request& req, COMMAND_RPC_ACCESS_ACCOUNT::response& res, epee::json_rpc::error& error_resp, const connection_context *ctx = NULL);
//-----------------------
private:
@ -252,6 +269,8 @@ private:
enum invoke_http_mode { JON, BIN, JON_RPC };
template <typename COMMAND_TYPE>
bool use_bootstrap_daemon_if_necessary(const invoke_http_mode &mode, const std::string &command_name, const typename COMMAND_TYPE::request& req, typename COMMAND_TYPE::response& res, bool &r);
bool get_block_template(const account_public_address &address, const crypto::hash *prev_block, const cryptonote::blobdata &extra_nonce, size_t &reserved_offset, cryptonote::difficulty_type &difficulty, uint64_t &height, uint64_t &expected_reward, block &b, uint64_t &seed_height, crypto::hash &seed_hash, crypto::hash &next_seed_hash, epee::json_rpc::error &error_resp);
bool check_payment(const std::string &client, uint64_t payment, const std::string &rpc, bool same_ts, std::string &message, uint64_t &credits, std::string &top_hash);
core& m_core;
nodetool::node_server<cryptonote::t_cryptonote_protocol_handler<cryptonote::core> >& m_p2p;
@ -260,10 +279,10 @@ private:
bool m_should_use_bootstrap_daemon;
std::chrono::system_clock::time_point m_bootstrap_height_check_time;
bool m_was_bootstrap_ever_used;
network_type m_nettype;
bool m_restricted;
epee::critical_section m_host_fails_score_lock;
std::map<std::string, uint64_t> m_host_fails_score;
std::unique_ptr<rpc_payment> m_rpc_payment;
};
}

File diff suppressed because it is too large Load diff

View file

@ -43,5 +43,34 @@
#define CORE_RPC_ERROR_CODE_UNSUPPORTED_RPC -11
#define CORE_RPC_ERROR_CODE_MINING_TO_SUBADDRESS -12
#define CORE_RPC_ERROR_CODE_REGTEST_REQUIRED -13
#define CORE_RPC_ERROR_CODE_PAYMENT_REQUIRED -14
#define CORE_RPC_ERROR_CODE_INVALID_CLIENT -15
#define CORE_RPC_ERROR_CODE_PAYMENT_TOO_LOW -16
#define CORE_RPC_ERROR_CODE_DUPLICATE_PAYMENT -17
#define CORE_RPC_ERROR_CODE_STALE_PAYMENT -18
static inline const char *get_rpc_server_error_message(int64_t code)
{
switch (code)
{
case CORE_RPC_ERROR_CODE_WRONG_PARAM: return "Invalid parameter";
case CORE_RPC_ERROR_CODE_TOO_BIG_HEIGHT: return "Height is too large";
case CORE_RPC_ERROR_CODE_TOO_BIG_RESERVE_SIZE: return "Reserve size is too large";
case CORE_RPC_ERROR_CODE_WRONG_WALLET_ADDRESS: return "Wrong wallet address";
case CORE_RPC_ERROR_CODE_INTERNAL_ERROR: return "Internal error";
case CORE_RPC_ERROR_CODE_WRONG_BLOCKBLOB: return "Wrong block blob";
case CORE_RPC_ERROR_CODE_BLOCK_NOT_ACCEPTED: return "Block not accepted";
case CORE_RPC_ERROR_CODE_CORE_BUSY: return "Core is busy";
case CORE_RPC_ERROR_CODE_WRONG_BLOCKBLOB_SIZE: return "Wrong block blob size";
case CORE_RPC_ERROR_CODE_UNSUPPORTED_RPC: return "Unsupported RPC";
case CORE_RPC_ERROR_CODE_MINING_TO_SUBADDRESS: return "Mining to subaddress is not supported";
case CORE_RPC_ERROR_CODE_REGTEST_REQUIRED: return "Regtest mode required";
case CORE_RPC_ERROR_CODE_PAYMENT_REQUIRED: return "Payment required";
case CORE_RPC_ERROR_CODE_INVALID_CLIENT: return "Invalid client";
case CORE_RPC_ERROR_CODE_PAYMENT_TOO_LOW: return "Payment too low";
case CORE_RPC_ERROR_CODE_DUPLICATE_PAYMENT: return "Duplicate payment";
case CORE_RPC_ERROR_CODE_STALE_PAYMENT: return "Stale payment";
default: MERROR("Unknown error: " << code); return "Unknown error";
}
}

View file

@ -80,6 +80,7 @@ namespace rpc
uint32_t ip;
uint16_t port;
uint16_t rpc_port;
uint32_t rpc_credits_per_hash;
uint64_t last_seen;
uint32_t pruning_seed;
};

402
src/rpc/rpc_payment.cpp Normal file
View file

@ -0,0 +1,402 @@
// Copyright (c) 2018-2019, The Monero Project
//
// All rights reserved.
//
// Redistribution and use in source and binary forms, with or without modification, are
// permitted provided that the following conditions are met:
//
// 1. Redistributions of source code must retain the above copyright notice, this list of
// conditions and the following disclaimer.
//
// 2. Redistributions in binary form must reproduce the above copyright notice, this list
// of conditions and the following disclaimer in the documentation and/or other
// materials provided with the distribution.
//
// 3. Neither the name of the copyright holder nor the names of its contributors may be
// used to endorse or promote products derived from this software without specific
// prior written permission.
//
// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY
// EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
// MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL
// THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
// PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
// INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
// STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF
// THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
#include <boost/archive/portable_binary_iarchive.hpp>
#include <boost/archive/portable_binary_oarchive.hpp>
#include "cryptonote_config.h"
#include "include_base_utils.h"
#include "string_tools.h"
#include "file_io_utils.h"
#include "int-util.h"
#include "common/util.h"
#include "serialization/crypto.h"
#include "common/unordered_containers_boost_serialization.h"
#include "cryptonote_basic/cryptonote_boost_serialization.h"
#include "cryptonote_basic/cryptonote_format_utils.h"
#include "cryptonote_basic/difficulty.h"
#include "core_rpc_server_error_codes.h"
#include "rpc_payment.h"
#undef MONERO_DEFAULT_LOG_CATEGORY
#define MONERO_DEFAULT_LOG_CATEGORY "daemon.rpc.payment"
#define STALE_THRESHOLD 15 /* seconds */
#define PENALTY_FOR_STALE 0
#define PENALTY_FOR_BAD_HASH 20
#define PENALTY_FOR_DUPLICATE 20
#define DEFAULT_FLUSH_AGE (3600 * 24 * 180) // half a year
#define DEFAULT_ZERO_FLUSH_AGE (60 * 2) // 2 minutes
#define RPC_PAYMENT_NONCE_TAIL 0x58
namespace cryptonote
{
rpc_payment::client_info::client_info():
cookie(0),
top(crypto::null_hash),
previous_top(crypto::null_hash),
credits(0),
update_time(time(NULL)),
last_request_timestamp(0),
block_template_update_time(0),
credits_total(0),
credits_used(0),
nonces_good(0),
nonces_stale(0),
nonces_bad(0),
nonces_dupe(0)
{
}
rpc_payment::rpc_payment(const cryptonote::account_public_address &address, uint64_t diff, uint64_t credits_per_hash_found):
m_address(address),
m_diff(diff),
m_credits_per_hash_found(credits_per_hash_found),
m_credits_total(0),
m_credits_used(0),
m_nonces_good(0),
m_nonces_stale(0),
m_nonces_bad(0),
m_nonces_dupe(0)
{
}
uint64_t rpc_payment::balance(const crypto::public_key &client, int64_t delta)
{
client_info &info = m_client_info[client]; // creates if not found
uint64_t credits = info.credits;
if (delta > 0 && credits > std::numeric_limits<uint64_t>::max() - delta)
credits = std::numeric_limits<uint64_t>::max();
else if (delta < 0 && credits < (uint64_t)-delta)
credits = 0;
else
credits += delta;
if (delta)
MINFO("Client " << client << ": balance change from " << info.credits << " to " << credits);
return info.credits = credits;
}
bool rpc_payment::pay(const crypto::public_key &client, uint64_t ts, uint64_t payment, const std::string &rpc, bool same_ts, uint64_t &credits)
{
client_info &info = m_client_info[client]; // creates if not found
if (ts < info.last_request_timestamp || (ts == info.last_request_timestamp && !same_ts))
{
MDEBUG("Invalid ts: " << ts << " <= " << info.last_request_timestamp);
return false;
}
info.last_request_timestamp = ts;
if (info.credits < payment)
{
MDEBUG("Not enough credits: " << info.credits << " < " << payment);
credits = info.credits;
return false;
}
info.credits -= payment;
add64clamp(&info.credits_used, payment);
add64clamp(&m_credits_used, payment);
MDEBUG("client " << client << " paying " << payment << " for " << rpc << ", " << info.credits << " left");
credits = info.credits;
return true;
}
bool rpc_payment::get_info(const crypto::public_key &client, const std::function<bool(const cryptonote::blobdata&, cryptonote::block&, uint64_t &seed_height, crypto::hash &seed_hash)> &get_block_template, cryptonote::blobdata &hashing_blob, uint64_t &seed_height, crypto::hash &seed_hash, const crypto::hash &top, uint64_t &diff, uint64_t &credits_per_hash_found, uint64_t &credits, uint32_t &cookie)
{
client_info &info = m_client_info[client]; // creates if not found
const uint64_t now = time(NULL);
bool need_template = top != info.top || now >= info.block_template_update_time + STALE_THRESHOLD;
if (need_template)
{
cryptonote::block new_block;
uint64_t new_seed_height;
crypto::hash new_seed_hash;
cryptonote::blobdata extra_nonce("\x42\x42\x42\x42", 4);
if (!get_block_template(extra_nonce, new_block, new_seed_height, new_seed_hash))
return false;
if(!remove_field_from_tx_extra(new_block.miner_tx.extra, typeid(cryptonote::tx_extra_nonce)))
return false;
char data[33];
memcpy(data, &client, 32);
data[32] = RPC_PAYMENT_NONCE_TAIL;
crypto::hash hash;
cn_fast_hash(data, sizeof(data), hash);
extra_nonce = cryptonote::blobdata((const char*)&hash, 4);
if(!add_extra_nonce_to_tx_extra(new_block.miner_tx.extra, extra_nonce))
return false;
info.previous_block = std::move(info.block);
info.block = std::move(new_block);
hashing_blob = get_block_hashing_blob(info.block);
info.previous_hashing_blob = info.hashing_blob;
info.hashing_blob = hashing_blob;
info.previous_top = info.top;
info.previous_seed_height = info.seed_height;
info.seed_height = new_seed_height;
info.previous_seed_hash = info.seed_hash;
info.seed_hash = new_seed_hash;
std::swap(info.previous_payments, info.payments);
info.payments.clear();
++info.cookie;
info.block_template_update_time = now;
}
info.top = top;
info.update_time = now;
hashing_blob = info.hashing_blob;
diff = m_diff;
credits_per_hash_found = m_credits_per_hash_found;
credits = info.credits;
seed_height = info.seed_height;
seed_hash = info.seed_hash;
cookie = info.cookie;
return true;
}
bool rpc_payment::submit_nonce(const crypto::public_key &client, uint32_t nonce, const crypto::hash &top, int64_t &error_code, std::string &error_message, uint64_t &credits, crypto::hash &hash, cryptonote::block &block, uint32_t cookie, bool &stale)
{
client_info &info = m_client_info[client]; // creates if not found
if (cookie != info.cookie && cookie != info.cookie - 1)
{
MWARNING("Very stale nonce");
++m_nonces_stale;
++info.nonces_stale;
sub64clamp(&info.credits, PENALTY_FOR_STALE * m_credits_per_hash_found);
error_code = CORE_RPC_ERROR_CODE_STALE_PAYMENT;
error_message = "Very stale payment";
return false;
}
const bool is_current = cookie == info.cookie;
MINFO("client " << client << " sends nonce: " << nonce << ", " << (is_current ? "current" : "stale"));
std::unordered_set<uint64_t> &payments = is_current ? info.payments : info.previous_payments;
if (!payments.insert(nonce).second)
{
MWARNING("Duplicate nonce " << nonce << " from " << (is_current ? "current" : "previous"));
++m_nonces_dupe;
++info.nonces_dupe;
sub64clamp(&info.credits, PENALTY_FOR_DUPLICATE * m_credits_per_hash_found);
error_code = CORE_RPC_ERROR_CODE_DUPLICATE_PAYMENT;
error_message = "Duplicate payment";
return false;
}
const uint64_t now = time(NULL);
if (!is_current)
{
if (now > info.update_time + STALE_THRESHOLD)
{
MWARNING("Nonce is stale (top " << top << ", should be " << info.top << " or within " << STALE_THRESHOLD << " seconds");
++m_nonces_stale;
++info.nonces_stale;
sub64clamp(&info.credits, PENALTY_FOR_STALE * m_credits_per_hash_found);
error_code = CORE_RPC_ERROR_CODE_STALE_PAYMENT;
error_message = "stale payment";
return false;
}
}
cryptonote::blobdata hashing_blob = is_current ? info.hashing_blob : info.previous_hashing_blob;
if (hashing_blob.size() < 43)
{
// not initialized ?
error_code = CORE_RPC_ERROR_CODE_WRONG_BLOCKBLOB;
error_message = "not initialized";
return false;
}
block = is_current ? info.block : info.previous_block;
*(uint32_t*)(hashing_blob.data() + 39) = SWAP32LE(nonce);
if (block.major_version >= RX_BLOCK_VERSION)
{
const uint64_t seed_height = is_current ? info.seed_height : info.previous_seed_height;
const crypto::hash &seed_hash = is_current ? info.seed_hash : info.previous_seed_hash;
const uint64_t height = cryptonote::get_block_height(block);
crypto::rx_slow_hash(height, seed_height, seed_hash.data, hashing_blob.data(), hashing_blob.size(), hash.data, 0, 0);
}
else
{
const int cn_variant = hashing_blob[0] >= 7 ? hashing_blob[0] - 6 : 0;
crypto::cn_slow_hash(hashing_blob.data(), hashing_blob.size(), hash, cn_variant, cryptonote::get_block_height(block));
}
if (!check_hash(hash, m_diff))
{
MWARNING("Payment too low");
++m_nonces_bad;
++info.nonces_bad;
error_code = CORE_RPC_ERROR_CODE_PAYMENT_TOO_LOW;
error_message = "Hash does not meet difficulty (could be wrong PoW hash, or mining at lower difficulty than required, or attempt to defraud)";
sub64clamp(&info.credits, PENALTY_FOR_BAD_HASH * m_credits_per_hash_found);
return false;
}
add64clamp(&info.credits, m_credits_per_hash_found);
MINFO("client " << client << " credited for " << m_credits_per_hash_found << ", now " << info.credits << (is_current ? "" : " (close)"));
m_hashrate[now] += m_diff;
add64clamp(&m_credits_total, m_credits_per_hash_found);
add64clamp(&info.credits_total, m_credits_per_hash_found);
++m_nonces_good;
++info.nonces_good;
credits = info.credits;
block = info.block;
block.nonce = nonce;
stale = !is_current;
return true;
}
bool rpc_payment::foreach(const std::function<bool(const crypto::public_key &client, const client_info &info)> &f) const
{
for (std::unordered_map<crypto::public_key, client_info>::const_iterator i = m_client_info.begin(); i != m_client_info.end(); ++i)
{
if (!f(i->first, i->second))
return false;
}
return true;
}
bool rpc_payment::load(std::string directory)
{
TRY_ENTRY();
m_directory = std::move(directory);
std::string state_file_path = directory + "/" + RPC_PAYMENTS_DATA_FILENAME;
MINFO("loading rpc payments data from " << state_file_path);
std::ifstream data;
data.open(state_file_path, std::ios_base::binary | std::ios_base::in);
if (!data.fail())
{
try
{
boost::archive::portable_binary_iarchive a(data);
a >> *this;
}
catch (const std::exception &e)
{
MERROR("Failed to load RPC payments file: " << e.what());
m_client_info.clear();
}
}
else
{
m_client_info.clear();
}
CATCH_ENTRY_L0("rpc_payment::load", false);
return true;
}
bool rpc_payment::store(const std::string &directory_) const
{
TRY_ENTRY();
const std::string &directory = directory_.empty() ? m_directory : directory_;
MDEBUG("storing rpc payments data to " << directory);
if (!tools::create_directories_if_necessary(directory))
{
MWARNING("Failed to create data directory: " << directory);
return false;
}
const boost::filesystem::path state_file_path = (boost::filesystem::path(directory) / RPC_PAYMENTS_DATA_FILENAME);
if (boost::filesystem::exists(state_file_path))
{
std::string state_file_path_old = state_file_path.string() + ".old";
boost::system::error_code ec;
boost::filesystem::remove(state_file_path_old, ec);
std::error_code e = tools::replace_file(state_file_path.string(), state_file_path_old);
if (e)
MWARNING("Failed to rename " << state_file_path << " to " << state_file_path_old << ": " << e);
}
std::ofstream data;
data.open(state_file_path.string(), std::ios_base::binary | std::ios_base::out | std::ios::trunc);
if (data.fail())
{
MWARNING("Failed to save RPC payments to file " << state_file_path);
return false;
};
boost::archive::portable_binary_oarchive a(data);
a << *this;
return true;
CATCH_ENTRY_L0("rpc_payment::store", false);
}
unsigned int rpc_payment::flush_by_age(time_t seconds)
{
unsigned int count = 0;
const time_t now = time(NULL);
time_t seconds0 = seconds;
if (seconds == 0)
{
seconds = DEFAULT_FLUSH_AGE;
seconds0 = DEFAULT_ZERO_FLUSH_AGE;
}
const time_t threshold = seconds > now ? 0 : now - seconds;
const time_t threshold0 = seconds0 > now ? 0 : now - seconds0;
for (std::unordered_map<crypto::public_key, client_info>::iterator i = m_client_info.begin(); i != m_client_info.end(); )
{
std::unordered_map<crypto::public_key, client_info>::iterator j = i++;
const time_t t = std::max(j->second.last_request_timestamp, j->second.update_time);
const bool erase = t < ((j->second.credits == 0) ? threshold0 : threshold);
if (erase)
{
MINFO("Erasing " << j->first << " with " << j->second.credits << " credits, inactive for " << (now-t)/86400 << " days");
m_client_info.erase(j);
++count;
}
}
return count;
}
uint64_t rpc_payment::get_hashes(unsigned int seconds) const
{
const uint64_t now = time(NULL);
uint64_t hashes = 0;
for (std::map<uint64_t, uint64_t>::const_reverse_iterator i = m_hashrate.crbegin(); i != m_hashrate.crend(); ++i)
{
if (now > i->first + seconds)
break;
hashes += i->second;
}
return hashes;
}
void rpc_payment::prune_hashrate(unsigned int seconds)
{
const uint64_t now = time(NULL);
std::map<uint64_t, uint64_t>::iterator i;
for (i = m_hashrate.begin(); i != m_hashrate.end(); ++i)
{
if (now <= i->first + seconds)
break;
}
m_hashrate.erase(m_hashrate.begin(), i);
}
bool rpc_payment::on_idle()
{
flush_by_age();
prune_hashrate(3600);
return true;
}
}

146
src/rpc/rpc_payment.h Normal file
View file

@ -0,0 +1,146 @@
// Copyright (c) 2018-2019, The Monero Project
//
// All rights reserved.
//
// Redistribution and use in source and binary forms, with or without modification, are
// permitted provided that the following conditions are met:
//
// 1. Redistributions of source code must retain the above copyright notice, this list of
// conditions and the following disclaimer.
//
// 2. Redistributions in binary form must reproduce the above copyright notice, this list
// of conditions and the following disclaimer in the documentation and/or other
// materials provided with the distribution.
//
// 3. Neither the name of the copyright holder nor the names of its contributors may be
// used to endorse or promote products derived from this software without specific
// prior written permission.
//
// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY
// EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
// MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL
// THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
// PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
// INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
// STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF
// THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
#pragma once
#include <string>
#include <unordered_set>
#include <unordered_map>
#include <boost/serialization/version.hpp>
#include "cryptonote_basic/blobdatatype.h"
#include "cryptonote_basic/cryptonote_basic.h"
namespace cryptonote
{
class rpc_payment
{
public:
struct client_info
{
cryptonote::block block;
cryptonote::block previous_block;
cryptonote::blobdata hashing_blob;
cryptonote::blobdata previous_hashing_blob;
uint64_t previous_seed_height;
uint64_t seed_height;
crypto::hash previous_seed_hash;
crypto::hash seed_hash;
uint32_t cookie;
crypto::hash top;
crypto::hash previous_top;
uint64_t credits;
std::unordered_set<uint64_t> payments;
std::unordered_set<uint64_t> previous_payments;
uint64_t update_time;
uint64_t last_request_timestamp;
uint64_t block_template_update_time;
uint64_t credits_total;
uint64_t credits_used;
uint64_t nonces_good;
uint64_t nonces_stale;
uint64_t nonces_bad;
uint64_t nonces_dupe;
client_info();
template <class t_archive>
inline void serialize(t_archive &a, const unsigned int ver)
{
a & block;
a & previous_block;
a & hashing_blob;
a & previous_hashing_blob;
a & seed_height;
a & previous_seed_height;
a & seed_hash;
a & previous_seed_hash;
a & cookie;
a & top;
a & previous_top;
a & credits;
a & payments;
a & previous_payments;
a & update_time;
a & last_request_timestamp;
a & block_template_update_time;
a & credits_total;
a & credits_used;
a & nonces_good;
a & nonces_stale;
a & nonces_bad;
a & nonces_dupe;
}
};
public:
rpc_payment(const cryptonote::account_public_address &address, uint64_t diff, uint64_t credits_per_hash_found);
uint64_t balance(const crypto::public_key &client, int64_t delta = 0);
bool pay(const crypto::public_key &client, uint64_t ts, uint64_t payment, const std::string &rpc, bool same_ts, uint64_t &credits);
bool get_info(const crypto::public_key &client, const std::function<bool(const cryptonote::blobdata&, cryptonote::block&, uint64_t &seed_height, crypto::hash &seed_hash)> &get_block_template, cryptonote::blobdata &hashing_blob, uint64_t &seed_height, crypto::hash &seed_hash, const crypto::hash &top, uint64_t &diff, uint64_t &credits_per_hash_found, uint64_t &credits, uint32_t &cookie);
bool submit_nonce(const crypto::public_key &client, uint32_t nonce, const crypto::hash &top, int64_t &error_code, std::string &error_message, uint64_t &credits, crypto::hash &hash, cryptonote::block &block, uint32_t cookie, bool &stale);
const cryptonote::account_public_address &get_payment_address() const { return m_address; }
bool foreach(const std::function<bool(const crypto::public_key &client, const client_info &info)> &f) const;
unsigned int flush_by_age(time_t seconds = 0);
uint64_t get_hashes(unsigned int seconds) const;
void prune_hashrate(unsigned int seconds);
bool on_idle();
template <class t_archive>
inline void serialize(t_archive &a, const unsigned int ver)
{
a & m_client_info;
a & m_hashrate;
a & m_credits_total;
a & m_credits_used;
a & m_nonces_good;
a & m_nonces_stale;
a & m_nonces_bad;
a & m_nonces_dupe;
}
bool load(std::string directory);
bool store(const std::string &directory = std::string()) const;
private:
cryptonote::account_public_address m_address;
uint64_t m_diff;
uint64_t m_credits_per_hash_found;
std::unordered_map<crypto::public_key, client_info> m_client_info;
std::string m_directory;
std::map<uint64_t, uint64_t> m_hashrate;
uint64_t m_credits_total;
uint64_t m_credits_used;
uint64_t m_nonces_good;
uint64_t m_nonces_stale;
uint64_t m_nonces_bad;
uint64_t m_nonces_dupe;
};
}
BOOST_CLASS_VERSION(cryptonote::rpc_payment, 0);
BOOST_CLASS_VERSION(cryptonote::rpc_payment::client_info, 0);

View file

@ -0,0 +1,49 @@
// Copyright (c) 2019, The Monero Project
//
// All rights reserved.
//
// Redistribution and use in source and binary forms, with or without modification, are
// permitted provided that the following conditions are met:
//
// 1. Redistributions of source code must retain the above copyright notice, this list of
// conditions and the following disclaimer.
//
// 2. Redistributions in binary form must reproduce the above copyright notice, this list
// of conditions and the following disclaimer in the documentation and/or other
// materials provided with the distribution.
//
// 3. Neither the name of the copyright holder nor the names of its contributors may be
// used to endorse or promote products derived from this software without specific
// prior written permission.
//
// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY
// EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
// MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL
// THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
// PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
// INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
// STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF
// THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
#pragma once
#define COST_PER_BLOCK 0.05
#define COST_PER_TX_RELAY 100
#define COST_PER_OUT 1
#define COST_PER_OUTPUT_INDEXES 1
#define COST_PER_TX 0.5
#define COST_PER_KEY_IMAGE 0.01
#define COST_PER_POOL_HASH 0.01
#define COST_PER_TX_POOL_STATS 0.2
#define COST_PER_BLOCK_HEADER 0.1
#define COST_PER_GET_INFO 1
#define COST_PER_OUTPUT_HISTOGRAM 25000
#define COST_PER_FULL_OUTPUT_HISTOGRAM 5000000
#define COST_PER_OUTPUT_DISTRIBUTION_0 20
#define COST_PER_OUTPUT_DISTRIBUTION 50000
#define COST_PER_COINBASE_TX_SUM_BLOCK 2
#define COST_PER_BLOCK_HASH 0.002
#define COST_PER_FEE_ESTIMATE 1
#define COST_PER_SYNC_INFO 2
#define COST_PER_HARD_FORK_INFO 1

View file

@ -0,0 +1,107 @@
// Copyright (c) 2018-2019, The Monero Project
//
// All rights reserved.
//
// Redistribution and use in source and binary forms, with or without modification, are
// permitted provided that the following conditions are met:
//
// 1. Redistributions of source code must retain the above copyright notice, this list of
// conditions and the following disclaimer.
//
// 2. Redistributions in binary form must reproduce the above copyright notice, this list
// of conditions and the following disclaimer in the documentation and/or other
// materials provided with the distribution.
//
// 3. Neither the name of the copyright holder nor the names of its contributors may be
// used to endorse or promote products derived from this software without specific
// prior written permission.
//
// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY
// EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
// MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL
// THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
// PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
// INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
// STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF
// THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
#include <inttypes.h>
#include <stdlib.h>
#include <chrono>
#include "include_base_utils.h"
#include "string_tools.h"
#include "rpc_payment_signature.h"
#undef MONERO_DEFAULT_LOG_CATEGORY
#define MONERO_DEFAULT_LOG_CATEGORY "daemon.rpc.payment"
#define TIMESTAMP_LEEWAY (60 * 1000000) /* 60 seconds, in microseconds */
namespace cryptonote
{
std::string make_rpc_payment_signature(const crypto::secret_key &skey)
{
std::string s;
crypto::public_key pkey;
crypto::secret_key_to_public_key(skey, pkey);
crypto::signature sig;
const uint64_t now = std::chrono::duration_cast<std::chrono::microseconds>(std::chrono::system_clock::now().time_since_epoch()).count();
char ts[17];
int ret = snprintf(ts, sizeof(ts), "%16.16" PRIx64, now);
CHECK_AND_ASSERT_MES(ret == 16, "", "snprintf failed");
ts[16] = 0;
CHECK_AND_ASSERT_MES(strlen(ts) == 16, "", "Invalid time conversion");
crypto::hash hash;
crypto::cn_fast_hash(ts, 16, hash);
crypto::generate_signature(hash, pkey, skey, sig);
s = epee::string_tools::pod_to_hex(pkey) + ts + epee::string_tools::pod_to_hex(sig);
return s;
}
bool verify_rpc_payment_signature(const std::string &message, crypto::public_key &pkey, uint64_t &ts)
{
if (message.size() != 2 * sizeof(crypto::public_key) + 16 + 2 * sizeof(crypto::signature))
{
MDEBUG("Bad message size: " << message.size());
return false;
}
const std::string pkey_string = message.substr(0, 2 * sizeof(crypto::public_key));
const std::string ts_string = message.substr(2 * sizeof(crypto::public_key), 16);
const std::string signature_string = message.substr(2 * sizeof(crypto::public_key) + 16);
if (!epee::string_tools::hex_to_pod(pkey_string, pkey))
{
MDEBUG("Bad client id");
return false;
}
crypto::signature signature;
if (!epee::string_tools::hex_to_pod(signature_string, signature))
{
MDEBUG("Bad signature");
return false;
}
crypto::hash hash;
crypto::cn_fast_hash(ts_string.data(), 16, hash);
if (!crypto::check_signature(hash, pkey, signature))
{
MDEBUG("signature does not verify");
return false;
}
char *endptr = NULL;
errno = 0;
unsigned long long ull = strtoull(ts_string.c_str(), &endptr, 16);
if (ull == ULLONG_MAX && errno == ERANGE)
{
MDEBUG("bad timestamp");
return false;
}
ts = ull;
const uint64_t now = std::chrono::duration_cast<std::chrono::microseconds>(std::chrono::system_clock::now().time_since_epoch()).count();
if (ts > now + TIMESTAMP_LEEWAY)
{
MDEBUG("Timestamp is in the future");
return false;
}
return true;
}
}

View file

@ -0,0 +1,39 @@
// Copyright (c) 2018-2019, The Monero Project
//
// All rights reserved.
//
// Redistribution and use in source and binary forms, with or without modification, are
// permitted provided that the following conditions are met:
//
// 1. Redistributions of source code must retain the above copyright notice, this list of
// conditions and the following disclaimer.
//
// 2. Redistributions in binary form must reproduce the above copyright notice, this list
// of conditions and the following disclaimer in the documentation and/or other
// materials provided with the distribution.
//
// 3. Neither the name of the copyright holder nor the names of its contributors may be
// used to endorse or promote products derived from this software without specific
// prior written permission.
//
// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY
// EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
// MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL
// THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
// PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
// INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
// STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF
// THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
#pragma once
#include <stdint.h>
#include <string>
#include "crypto/crypto.h"
namespace cryptonote
{
std::string make_rpc_payment_signature(const crypto::secret_key &skey);
bool verify_rpc_payment_signature(const std::string &message, crypto::public_key &pkey, uint64_t &ts);
}

View file

@ -571,6 +571,7 @@ void toJsonValue(rapidjson::Document& doc, const cryptonote::connection_info& in
INSERT_INTO_JSON_OBJECT(val, doc, ip, info.ip);
INSERT_INTO_JSON_OBJECT(val, doc, port, info.port);
INSERT_INTO_JSON_OBJECT(val, doc, rpc_port, info.rpc_port);
INSERT_INTO_JSON_OBJECT(val, doc, rpc_credits_per_hash, info.rpc_credits_per_hash);
INSERT_INTO_JSON_OBJECT(val, doc, peer_id, info.peer_id);
@ -607,6 +608,7 @@ void fromJsonValue(const rapidjson::Value& val, cryptonote::connection_info& inf
GET_FROM_JSON_OBJECT(val, info.ip, ip);
GET_FROM_JSON_OBJECT(val, info.port, port);
GET_FROM_JSON_OBJECT(val, info.rpc_port, rpc_port);
GET_FROM_JSON_OBJECT(val, info.rpc_credits_per_hash, rpc_credits_per_hash);
GET_FROM_JSON_OBJECT(val, info.peer_id, peer_id);
@ -756,6 +758,7 @@ void toJsonValue(rapidjson::Document& doc, const cryptonote::rpc::peer& peer, ra
INSERT_INTO_JSON_OBJECT(val, doc, ip, peer.ip);
INSERT_INTO_JSON_OBJECT(val, doc, port, peer.port);
INSERT_INTO_JSON_OBJECT(val, doc, rpc_port, peer.rpc_port);
INSERT_INTO_JSON_OBJECT(val, doc, rpc_credits_per_hash, peer.rpc_credits_per_hash);
INSERT_INTO_JSON_OBJECT(val, doc, last_seen, peer.last_seen);
INSERT_INTO_JSON_OBJECT(val, doc, pruning_seed, peer.pruning_seed);
}
@ -772,6 +775,7 @@ void fromJsonValue(const rapidjson::Value& val, cryptonote::rpc::peer& peer)
GET_FROM_JSON_OBJECT(val, peer.ip, ip);
GET_FROM_JSON_OBJECT(val, peer.port, port);
GET_FROM_JSON_OBJECT(val, peer.rpc_port, rpc_port);
GET_FROM_JSON_OBJECT(val, peer.rpc_credits_per_hash, rpc_credits_per_hash);
GET_FROM_JSON_OBJECT(val, peer.last_seen, last_seen);
GET_FROM_JSON_OBJECT(val, peer.pruning_seed, pruning_seed);
}

View file

@ -58,6 +58,7 @@
#include "cryptonote_basic/cryptonote_format_utils.h"
#include "storages/http_abstract_invoke.h"
#include "rpc/core_rpc_server_commands_defs.h"
#include "rpc/rpc_payment_signature.h"
#include "crypto/crypto.h" // for crypto::secret_key definition
#include "mnemonics/electrum-words.h"
#include "rapidjson/document.h"
@ -99,12 +100,17 @@ typedef cryptonote::simple_wallet sw;
#define LOCK_IDLE_SCOPE() \
bool auto_refresh_enabled = m_auto_refresh_enabled.load(std::memory_order_relaxed); \
m_auto_refresh_enabled.store(false, std::memory_order_relaxed); \
/* stop any background refresh, and take over */ \
/* stop any background refresh and other processes, and take over */ \
m_suspend_rpc_payment_mining.store(true, std::memory_order_relaxed); \
m_wallet->stop(); \
boost::unique_lock<boost::mutex> lock(m_idle_mutex); \
m_idle_cond.notify_all(); \
epee::misc_utils::auto_scope_leave_caller scope_exit_handler = epee::misc_utils::create_scope_leave_handler([&](){ \
/* m_idle_mutex is still locked here */ \
m_auto_refresh_enabled.store(auto_refresh_enabled, std::memory_order_relaxed); \
m_suspend_rpc_payment_mining.store(false, std::memory_order_relaxed);; \
m_rpc_payment_checker.trigger(); \
m_idle_cond.notify_one(); \
})
#define SCOPED_WALLET_UNLOCK_ON_BAD_PASSWORD(code) \
@ -125,15 +131,24 @@ typedef cryptonote::simple_wallet sw;
return true; \
} while(0)
#define REFRESH_PERIOD 90 // seconds
#define CREDITS_TARGET 50000
#define MAX_PAYMENT_DIFF 10000
#define MIN_PAYMENT_RATE 0.01f // per hash
enum TransferType {
Transfer,
TransferLocked,
};
static std::string get_human_readable_timespan(std::chrono::seconds seconds);
namespace
{
const std::array<const char* const, 5> allowed_priority_strings = {{"default", "unimportant", "normal", "elevated", "priority"}};
const auto arg_wallet_file = wallet_args::arg_wallet_file();
const auto arg_rpc_client_secret_key = wallet_args::arg_rpc_client_secret_key();
const command_line::arg_descriptor<std::string> arg_generate_new_wallet = {"generate-new-wallet", sw::tr("Generate new wallet and save it to <arg>"), ""};
const command_line::arg_descriptor<std::string> arg_generate_from_device = {"generate-from-device", sw::tr("Generate new wallet from device and save it to <arg>"), ""};
const command_line::arg_descriptor<std::string> arg_generate_from_view_key = {"generate-from-view-key", sw::tr("Generate incoming-only wallet from view key"), ""};
@ -248,6 +263,9 @@ namespace
const char* USAGE_LOCK("lock");
const char* USAGE_NET_STATS("net_stats");
const char* USAGE_WELCOME("welcome");
const char* USAGE_RPC_PAYMENT_INFO("rpc_payment_info");
const char* USAGE_START_MINING_FOR_RPC("start_mining_for_rpc");
const char* USAGE_STOP_MINING_FOR_RPC("stop_mining_for_rpc");
const char* USAGE_VERSION("version");
const char* USAGE_HELP("help [<command>]");
@ -490,22 +508,28 @@ namespace
fail_msg_writer() << sw::tr("invalid format for subaddress lookahead; must be <major>:<minor>");
return r;
}
}
void handle_transfer_exception(const std::exception_ptr &e, bool trusted_daemon)
{
void simple_wallet::handle_transfer_exception(const std::exception_ptr &e, bool trusted_daemon)
{
bool warn_of_possible_attack = !trusted_daemon;
try
{
std::rethrow_exception(e);
}
catch (const tools::error::daemon_busy&)
catch (const tools::error::payment_required&)
{
fail_msg_writer() << sw::tr("daemon is busy. Please try again later.");
fail_msg_writer() << tr("Payment required, see the 'rpc_payment_info' command");
m_need_payment = true;
}
catch (const tools::error::no_connection_to_daemon&)
{
fail_msg_writer() << sw::tr("no connection to daemon. Please make sure daemon is running.");
}
catch (const tools::error::daemon_busy&)
{
fail_msg_writer() << tr("daemon is busy. Please try again later.");
}
catch (const tools::error::wallet_rpc_error& e)
{
LOG_ERROR("RPC error: " << e.to_string());
@ -602,8 +626,10 @@ namespace
if (warn_of_possible_attack)
fail_msg_writer() << sw::tr("There was an error, which could mean the node may be trying to get you to retry creating a transaction, and zero in on which outputs you own. Or it could be a bona fide error. It may be prudent to disconnect from this node, and not try to send a transaction immediately. Alternatively, connect to another node so the original node cannot correlate information.");
}
}
namespace
{
bool check_file_overwrite(const std::string &filename)
{
boost::system::error_code errcode;
@ -1908,6 +1934,77 @@ bool simple_wallet::unset_ring(const std::vector<std::string> &args)
return true;
}
bool simple_wallet::rpc_payment_info(const std::vector<std::string> &args)
{
if (!try_connect_to_daemon())
return true;
LOCK_IDLE_SCOPE();
try
{
bool payment_required;
uint64_t credits, diff, credits_per_hash_found, height, seed_height;
uint32_t cookie;
std::string hashing_blob;
crypto::hash seed_hash, next_seed_hash;
crypto::public_key pkey;
crypto::secret_key_to_public_key(m_wallet->get_rpc_client_secret_key(), pkey);
message_writer() << tr("RPC client ID: ") << pkey;
message_writer() << tr("RPC client secret key: ") << m_wallet->get_rpc_client_secret_key();
if (!m_wallet->get_rpc_payment_info(false, payment_required, credits, diff, credits_per_hash_found, hashing_blob, height, seed_height, seed_hash, next_seed_hash, cookie))
{
fail_msg_writer() << tr("Failed to query daemon");
return true;
}
if (payment_required)
{
uint64_t target = m_wallet->credits_target();
if (target == 0)
target = CREDITS_TARGET;
message_writer() << tr("Using daemon: ") << m_wallet->get_daemon_address();
message_writer() << tr("Payments required for node use, current credits: ") << credits;
message_writer() << tr("Credits target: ") << target;
uint64_t expected, discrepancy;
m_wallet->credit_report(expected, discrepancy);
message_writer() << tr("Credits spent this session: ") << expected;
if (expected)
message_writer() << tr("Credit discrepancy this session: ") << discrepancy << " (" << 100.0f * discrepancy / expected << "%)";
float cph = credits_per_hash_found / (float)diff;
message_writer() << tr("Difficulty: ") << diff << ", " << credits_per_hash_found << " " << tr("credits per hash found, ") << cph << " " << tr("credits/hash");;
const boost::posix_time::ptime now = boost::posix_time::microsec_clock::universal_time();
bool mining = (now - m_last_rpc_payment_mining_time).total_microseconds() < 1000000;
if (mining)
{
float hash_rate = m_rpc_payment_hash_rate;
if (hash_rate > 0)
{
message_writer() << (boost::format(tr("Mining for payment at %.1f H/s")) % hash_rate).str();
if (credits < target)
{
std::chrono::seconds seconds((unsigned)((target - credits) / cph / hash_rate));
std::string target_string = get_human_readable_timespan(seconds);
message_writer() << (boost::format(tr("Estimated time till %u credits target mined: %s")) % target % target_string).str();
}
}
else
message_writer() << tr("Mining for payment");
}
else
message_writer() << tr("Not mining");
}
else
message_writer() << tr("No payment needed for node use");
}
catch (const std::exception& e)
{
LOG_ERROR("unexpected error: " << e.what());
fail_msg_writer() << tr("unexpected error: ") << e.what();
}
return true;
}
bool simple_wallet::blackball(const std::vector<std::string> &args)
{
uint64_t amount = std::numeric_limits<uint64_t>::max(), offset, num_offsets;
@ -2214,6 +2311,50 @@ bool simple_wallet::cold_sign_tx(const std::vector<tools::wallet2::pending_tx>&
return m_wallet->import_key_images(exported_txs, 0, true);
}
bool simple_wallet::start_mining_for_rpc(const std::vector<std::string> &args)
{
if (!try_connect_to_daemon())
return true;
LOCK_IDLE_SCOPE();
bool payment_required;
uint64_t credits, diff, credits_per_hash_found, height, seed_height;
uint32_t cookie;
std::string hashing_blob;
crypto::hash seed_hash, next_seed_hash;
if (!m_wallet->get_rpc_payment_info(true, payment_required, credits, diff, credits_per_hash_found, hashing_blob, height, seed_height, seed_hash, next_seed_hash, cookie))
{
fail_msg_writer() << tr("Failed to query daemon");
return true;
}
if (!payment_required)
{
fail_msg_writer() << tr("Daemon does not require payment for RPC access");
return true;
}
m_rpc_payment_mining_requested = true;
m_rpc_payment_checker.trigger();
const float cph = credits_per_hash_found / (float)diff;
bool low = (diff > MAX_PAYMENT_DIFF || cph < MIN_PAYMENT_RATE);
success_msg_writer() << (boost::format(tr("Starting mining for RPC access: diff %llu, %f credits/hash%s")) % diff % cph % (low ? " - this is low" : "")).str();
success_msg_writer() << tr("Run stop_mining_for_rpc to stop");
return true;
}
bool simple_wallet::stop_mining_for_rpc(const std::vector<std::string> &args)
{
if (!try_connect_to_daemon())
return true;
LOCK_IDLE_SCOPE();
m_rpc_payment_mining_requested = false;
m_last_rpc_payment_mining_time = boost::posix_time::ptime(boost::gregorian::date(1970, 1, 1));
m_rpc_payment_hash_rate = -1.0f;
return true;
}
bool simple_wallet::set_always_confirm_transfers(const std::vector<std::string> &args/* = std::vector<std::string>()*/)
{
const auto pwd_container = get_and_verify_password();
@ -2602,6 +2743,53 @@ bool simple_wallet::set_segregate_pre_fork_outputs(const std::vector<std::string
return true;
}
bool simple_wallet::set_persistent_rpc_client_id(const std::vector<std::string> &args/* = std::vector<std::string>()*/)
{
const auto pwd_container = get_and_verify_password();
if (pwd_container)
{
parse_bool_and_use(args[1], [&](bool r) {
m_wallet->persistent_rpc_client_id(r);
m_wallet->rewrite(m_wallet_file, pwd_container->password());
});
}
return true;
}
bool simple_wallet::set_auto_mine_for_rpc_payment_threshold(const std::vector<std::string> &args/* = std::vector<std::string>()*/)
{
const auto pwd_container = get_and_verify_password();
if (pwd_container)
{
float threshold;
if (!epee::string_tools::get_xtype_from_string(threshold, args[1]) || threshold < 0.0f)
{
fail_msg_writer() << tr("Invalid threshold");
return true;
}
m_wallet->auto_mine_for_rpc_payment_threshold(threshold);
m_wallet->rewrite(m_wallet_file, pwd_container->password());
}
return true;
}
bool simple_wallet::set_credits_target(const std::vector<std::string> &args/* = std::vector<std::string>()*/)
{
const auto pwd_container = get_and_verify_password();
if (pwd_container)
{
uint64_t target;
if (!epee::string_tools::get_xtype_from_string(target, args[1]))
{
fail_msg_writer() << tr("Invalid target");
return true;
}
m_wallet->credits_target(target);
m_wallet->rewrite(m_wallet_file, pwd_container->password());
}
return true;
}
bool simple_wallet::set_key_reuse_mitigation2(const std::vector<std::string> &args/* = std::vector<std::string>()*/)
{
const auto pwd_container = get_and_verify_password();
@ -2844,6 +3032,12 @@ simple_wallet::simple_wallet()
, m_last_activity_time(time(NULL))
, m_locked(false)
, m_in_command(false)
, m_need_payment(false)
, m_rpc_payment_mining_requested(false)
, m_last_rpc_payment_mining_time(boost::gregorian::date(1970, 1, 1))
, m_daemon_rpc_payment_message_displayed(false)
, m_rpc_payment_hash_rate(-1.0f)
, m_suspend_rpc_payment_mining(false)
{
m_cmd_binder.set_handler("start_mining",
boost::bind(&simple_wallet::on_command, this, &simple_wallet::start_mining, _1),
@ -3022,7 +3216,13 @@ simple_wallet::simple_wallet()
"device-name <device_name[:device_spec]>\n "
" Device name for hardware wallet.\n "
"export-format <\"binary\"|\"ascii\">\n "
" Save all exported files as binary (cannot be copied and pasted) or ascii (can be).\n "));
" Save all exported files as binary (cannot be copied and pasted) or ascii (can be).\n "
"persistent-client-id <1|0>\n "
" Whether to keep using the same client id for RPC payment over wallet restarts.\n"
"auto-mine-for-rpc-payment-threshold <float>\n "
" Whether to automatically start mining for RPC payment if the daemon requires it.\n"
"credits-target <unsigned int>\n"
" The RPC payment credits balance to target (0 for default)."));
m_cmd_binder.set_handler("encrypted_seed",
boost::bind(&simple_wallet::on_command, this, &simple_wallet::encrypted_seed, _1),
tr("Display the encrypted Electrum-style mnemonic seed."));
@ -3333,6 +3533,18 @@ simple_wallet::simple_wallet()
boost::bind(&simple_wallet::on_command, this, &simple_wallet::version, _1),
tr(USAGE_VERSION),
tr("Returns version information"));
m_cmd_binder.set_handler("rpc_payment_info",
boost::bind(&simple_wallet::rpc_payment_info, this, _1),
tr(USAGE_RPC_PAYMENT_INFO),
tr("Get info about RPC payments to current node"));
m_cmd_binder.set_handler("start_mining_for_rpc",
boost::bind(&simple_wallet::start_mining_for_rpc, this, _1),
tr(USAGE_START_MINING_FOR_RPC),
tr("Start mining to pay for RPC access"));
m_cmd_binder.set_handler("stop_mining_for_rpc",
boost::bind(&simple_wallet::stop_mining_for_rpc, this, _1),
tr(USAGE_STOP_MINING_FOR_RPC),
tr("Stop mining to pay for RPC access"));
m_cmd_binder.set_handler("help",
boost::bind(&simple_wallet::on_command, this, &simple_wallet::help, _1),
tr(USAGE_HELP),
@ -3402,6 +3614,9 @@ bool simple_wallet::set_variable(const std::vector<std::string> &args)
<< " (disabled on Windows)"
#endif
;
success_msg_writer() << "persistent-rpc-client-id = " << m_wallet->persistent_rpc_client_id();
success_msg_writer() << "auto-mine-for-rpc-payment-threshold = " << m_wallet->auto_mine_for_rpc_payment_threshold();
success_msg_writer() << "credits-target = " << m_wallet->credits_target();
return true;
}
else
@ -3463,6 +3678,9 @@ bool simple_wallet::set_variable(const std::vector<std::string> &args)
CHECK_SIMPLE_VARIABLE("setup-background-mining", set_setup_background_mining, tr("1/yes or 0/no"));
CHECK_SIMPLE_VARIABLE("device-name", set_device_name, tr("<device_name[:device_spec]>"));
CHECK_SIMPLE_VARIABLE("export-format", set_export_format, tr("\"binary\" or \"ascii\""));
CHECK_SIMPLE_VARIABLE("persistent-rpc-client-id", set_persistent_rpc_client_id, tr("0 or 1"));
CHECK_SIMPLE_VARIABLE("auto-mine-for-rpc-payment-threshold", set_auto_mine_for_rpc_payment_threshold, tr("floating point >= 0"));
CHECK_SIMPLE_VARIABLE("credits-target", set_credits_target, tr("unsigned integer"));
}
fail_msg_writer() << tr("set: unrecognized argument(s)");
return true;
@ -4227,6 +4445,17 @@ bool simple_wallet::init(const boost::program_options::variables_map& vm)
return false;
}
if (!command_line::is_arg_defaulted(vm, arg_rpc_client_secret_key))
{
crypto::secret_key rpc_client_secret_key;
if (!epee::string_tools::hex_to_pod(command_line::get_arg(vm, arg_rpc_client_secret_key), rpc_client_secret_key))
{
fail_msg_writer() << tr("RPC client secret key should be 32 byte in hex format");
return false;
}
m_wallet->set_rpc_client_secret_key(rpc_client_secret_key);
}
if (!m_wallet->is_trusted_daemon())
{
message_writer(console_color_red, true) << (boost::format(tr("Warning: using an untrusted daemon at %s")) % m_wallet->get_daemon_address()).str();
@ -4742,6 +4971,7 @@ bool simple_wallet::close_wallet()
if (m_idle_run.load(std::memory_order_relaxed))
{
m_idle_run.store(false, std::memory_order_relaxed);
m_suspend_rpc_payment_mining.store(true, std::memory_order_relaxed);
m_wallet->stop();
{
boost::unique_lock<boost::mutex> lock(m_idle_mutex);
@ -5009,6 +5239,57 @@ bool simple_wallet::stop_mining(const std::vector<std::string>& args)
return true;
}
//----------------------------------------------------------------------------------------------------
bool simple_wallet::check_daemon_rpc_prices(const std::string &daemon_url, uint32_t &actual_cph, uint32_t &claimed_cph)
{
try
{
auto i = m_claimed_cph.find(daemon_url);
if (i == m_claimed_cph.end())
return false;
claimed_cph = m_claimed_cph[daemon_url];
bool payment_required;
uint64_t credits, diff, credits_per_hash_found, height, seed_height;
uint32_t cookie;
cryptonote::blobdata hashing_blob;
crypto::hash seed_hash, next_seed_hash;
if (m_wallet->get_rpc_payment_info(false, payment_required, credits, diff, credits_per_hash_found, hashing_blob, height, seed_height, seed_hash, next_seed_hash, cookie) && payment_required)
{
actual_cph = RPC_CREDITS_PER_HASH_SCALE * (credits_per_hash_found / (float)diff);
return true;
}
else
{
const std::string host = node.host + ":" + std::to_string(node.rpc_port);
if (host == daemon_url)
{
claimed_cph = node.rpc_credits_per_hash;
bool payment_required;
uint64_t credits, diff, credits_per_hash_found, height;
uint32_t cookie;
cryptonote::blobdata hashing_blob;
if (m_wallet->get_rpc_payment_info(false, payment_required, credits, diff, credits_per_hash_found, hashing_blob, height, cookie) && payment_required)
{
actual_cph = RPC_CREDITS_PER_HASH_SCALE * (credits_per_hash_found / (float)diff);
return true;
}
else
{
fail_msg_writer() << tr("Error checking daemon RPC access prices");
}
}
}
}
catch (const std::exception &e)
{
// can't check
fail_msg_writer() << tr("Error checking daemon RPC access prices: ") << e.what();
return false;
}
// no record found for this daemon
return false;
}
//----------------------------------------------------------------------------------------------------
bool simple_wallet::set_daemon(const std::vector<std::string>& args)
{
std::string daemon_url;
@ -5066,6 +5347,8 @@ bool simple_wallet::set_daemon(const std::vector<std::string>& args)
catch (const std::exception &e) { }
}
success_msg_writer() << boost::format("Daemon set to %s, %s") % daemon_url % (m_wallet->is_trusted_daemon() ? tr("trusted") : tr("untrusted"));
m_daemon_rpc_payment_message_displayed = false;
} else {
fail_msg_writer() << tr("This does not seem to be a valid daemon URL.");
}
@ -5304,6 +5587,11 @@ bool simple_wallet::refresh_main(uint64_t start_height, enum ResetType reset, bo
{
ss << tr("no connection to daemon. Please make sure daemon is running.");
}
catch (const tools::error::payment_required&)
{
ss << tr("payment required.");
m_need_payment = true;
}
catch (const tools::error::wallet_rpc_error& e)
{
LOG_ERROR("RPC error: " << e.to_string());
@ -5630,6 +5918,11 @@ bool simple_wallet::rescan_spent(const std::vector<std::string> &args)
{
fail_msg_writer() << tr("no connection to daemon. Please make sure daemon is running.");
}
catch (const tools::error::payment_required&)
{
fail_msg_writer() << tr("payment required.");
m_need_payment = true;
}
catch (const tools::error::is_key_image_spent_error&)
{
fail_msg_writer() << tr("failed to get spent status");
@ -5733,6 +6026,7 @@ bool simple_wallet::print_ring_members(const std::vector<tools::wallet2::pending
req.outputs[j].index = absolute_offsets[j];
}
COMMAND_RPC_GET_OUTPUTS_BIN::response res = AUTO_VAL_INIT(res);
req.client = cryptonote::make_rpc_payment_signature(m_wallet->get_rpc_client_secret_key());
bool r = m_wallet->invoke_http_bin("/get_outs.bin", req, res);
err = interpret_rpc_response(r, res.status);
if (!err.empty())
@ -8474,6 +8768,7 @@ void simple_wallet::wallet_idle_thread()
#endif
m_refresh_checker.do_call(boost::bind(&simple_wallet::check_refresh, this));
m_mms_checker.do_call(boost::bind(&simple_wallet::check_mms, this));
m_rpc_payment_checker.do_call(boost::bind(&simple_wallet::check_rpc_payment, this));
if (!m_idle_run.load(std::memory_order_relaxed))
break;
@ -8527,6 +8822,78 @@ bool simple_wallet::check_mms()
return true;
}
//----------------------------------------------------------------------------------------------------
bool simple_wallet::check_rpc_payment()
{
if (!m_rpc_payment_mining_requested && m_wallet->auto_mine_for_rpc_payment_threshold() == 0.0f)
return true;
uint64_t target = m_wallet->credits_target();
if (target == 0)
target = CREDITS_TARGET;
if (m_rpc_payment_mining_requested)
target = std::numeric_limits<uint64_t>::max();
bool need_payment = m_need_payment || m_rpc_payment_mining_requested || (m_wallet->credits() < target && m_wallet->daemon_requires_payment());
if (need_payment)
{
const boost::posix_time::ptime start_time = boost::posix_time::microsec_clock::universal_time();
auto startfunc = [this](uint64_t diff, uint64_t credits_per_hash_found)
{
const float cph = credits_per_hash_found / (float)diff;
bool low = (diff > MAX_PAYMENT_DIFF || cph < MIN_PAYMENT_RATE);
if (credits_per_hash_found > 0 && cph >= m_wallet->auto_mine_for_rpc_payment_threshold())
{
MINFO(std::to_string(cph) << " credits per hash is >= our threshold (" << m_wallet->auto_mine_for_rpc_payment_threshold() << "), starting mining");
return true;
}
else if (m_rpc_payment_mining_requested)
{
MINFO("Mining for RPC payment was requested, starting mining");
return true;
}
else
{
if (!m_daemon_rpc_payment_message_displayed)
{
success_msg_writer() << boost::format(tr("Daemon requests payment at diff %llu, with %f credits/hash%s. Run start_mining_for_rpc to start mining to pay for RPC access, or use another daemon")) %
diff % cph % (low ? " - this is low" : "");
m_cmd_binder.print_prompt();
m_daemon_rpc_payment_message_displayed = true;
}
return false;
}
};
auto contfunc = [&,this](unsigned n_hashes)
{
if (m_suspend_rpc_payment_mining.load(std::memory_order_relaxed))
return false;
const boost::posix_time::ptime now = boost::posix_time::microsec_clock::universal_time();
m_last_rpc_payment_mining_time = now;
if ((now - start_time).total_microseconds() >= 2 * 1000000)
m_rpc_payment_hash_rate = n_hashes / (float)((now - start_time).total_seconds());
if ((now - start_time).total_microseconds() >= REFRESH_PERIOD * 1000000)
return false;
return true;
};
auto foundfunc = [this, target](uint64_t credits)
{
m_need_payment = false;
return credits < target;
};
auto errorfunc = [this](const std::string &error)
{
fail_msg_writer() << tr("Error mining to daemon: ") << error;
m_cmd_binder.print_prompt();
};
bool ret = m_wallet->search_for_rpc_payment(target, startfunc, contfunc, foundfunc, errorfunc);
if (!ret)
{
fail_msg_writer() << tr("Failed to start mining for RPC payment");
m_cmd_binder.print_prompt();
}
}
return true;
}
//----------------------------------------------------------------------------------------------------
std::string simple_wallet::get_prompt() const
{
if (m_locked)
@ -9728,6 +10095,7 @@ int main(int argc, char* argv[])
command_line::add_arg(desc_params, arg_create_address_file);
command_line::add_arg(desc_params, arg_subaddress_lookahead);
command_line::add_arg(desc_params, arg_use_english_language_names);
command_line::add_arg(desc_params, arg_rpc_client_secret_key);
po::positional_options_description positional_options;
positional_options.add(arg_command.name, -1);

View file

@ -151,6 +151,9 @@ namespace cryptonote
bool set_setup_background_mining(const std::vector<std::string> &args = std::vector<std::string>());
bool set_device_name(const std::vector<std::string> &args = std::vector<std::string>());
bool set_export_format(const std::vector<std::string> &args = std::vector<std::string>());
bool set_persistent_rpc_client_id(const std::vector<std::string> &args = std::vector<std::string>());
bool set_auto_mine_for_rpc_payment_threshold(const std::vector<std::string> &args = std::vector<std::string>());
bool set_credits_target(const std::vector<std::string> &args = std::vector<std::string>());
bool help(const std::vector<std::string> &args = std::vector<std::string>());
bool start_mining(const std::vector<std::string> &args);
bool stop_mining(const std::vector<std::string> &args);
@ -250,6 +253,9 @@ namespace cryptonote
bool thaw(const std::vector<std::string>& args);
bool frozen(const std::vector<std::string>& args);
bool lock(const std::vector<std::string>& args);
bool rpc_payment_info(const std::vector<std::string> &args);
bool start_mining_for_rpc(const std::vector<std::string> &args);
bool stop_mining_for_rpc(const std::vector<std::string> &args);
bool net_stats(const std::vector<std::string>& args);
bool welcome(const std::vector<std::string>& args);
bool version(const std::vector<std::string>& args);
@ -325,6 +331,9 @@ namespace cryptonote
bool check_inactivity();
bool check_refresh();
bool check_mms();
bool check_rpc_payment();
void handle_transfer_exception(const std::exception_ptr &e, bool trusted_daemon);
//----------------- i_wallet2_callback ---------------------
virtual void on_new_block(uint64_t height, const cryptonote::block& block);
@ -439,6 +448,14 @@ namespace cryptonote
epee::math_helper::once_a_time_seconds<1> m_inactivity_checker;
epee::math_helper::once_a_time_seconds<90> m_refresh_checker;
epee::math_helper::once_a_time_seconds<90> m_mms_checker;
epee::math_helper::once_a_time_seconds<90> m_rpc_payment_checker;
std::atomic<bool> m_need_payment;
boost::posix_time::ptime m_last_rpc_payment_mining_time;
bool m_rpc_payment_mining_requested;
bool m_daemon_rpc_payment_message_displayed;
float m_rpc_payment_hash_rate;
std::atomic<bool> m_suspend_rpc_payment_mining;
// MMS
mms::message_store& get_message_store() const { return m_wallet->get_message_store(); };

View file

@ -37,6 +37,7 @@ set(wallet_sources
node_rpc_proxy.cpp
message_store.cpp
message_transporter.cpp
wallet_rpc_payments.cpp
)
set(wallet_private_headers
@ -49,7 +50,8 @@ set(wallet_private_headers
ringdb.h
node_rpc_proxy.h
message_store.h
message_transporter.h)
message_transporter.h
wallet_rpc_helpers.h)
monero_private_headers(wallet
${wallet_private_headers})
@ -58,6 +60,7 @@ monero_add_library(wallet
${wallet_private_headers})
target_link_libraries(wallet
PUBLIC
rpc_base
multisig
common
cryptonote_core
@ -116,6 +119,7 @@ if (BUILD_GUI_DEPS)
set(libs_to_merge
wallet_api
wallet
rpc_base
multisig
blockchain_db
cryptonote_core

View file

@ -28,8 +28,22 @@
#include "node_rpc_proxy.h"
#include "rpc/core_rpc_server_commands_defs.h"
#include "rpc/rpc_payment_signature.h"
#include "rpc/rpc_payment_costs.h"
#include "storages/http_abstract_invoke.h"
#define RETURN_ON_RPC_RESPONSE_ERROR(r, error, res, method) \
do { \
CHECK_AND_ASSERT_MES(error.code == 0, error.message, error.message); \
handle_payment_changes(res, std::integral_constant<bool, HasCredits<decltype(res)>::Has>()); \
CHECK_AND_ASSERT_MES(r, std::string(), "Failed to connect to daemon"); \
/* empty string -> not connection */ \
CHECK_AND_ASSERT_MES(!res.status.empty(), res.status, "No connection to daemon"); \
CHECK_AND_ASSERT_MES(res.status != CORE_RPC_STATUS_BUSY, res.status, "Daemon busy"); \
CHECK_AND_ASSERT_MES(res.status != CORE_RPC_STATUS_PAYMENT_REQUIRED, res.status, "Payment required"); \
CHECK_AND_ASSERT_MES(res.status == CORE_RPC_STATUS_OK, res.status, "Error calling " + std::string(method) + " daemon RPC"); \
} while(0)
using namespace epee;
namespace tools
@ -37,8 +51,9 @@ namespace tools
static const std::chrono::seconds rpc_timeout = std::chrono::minutes(3) + std::chrono::seconds(30);
NodeRPCProxy::NodeRPCProxy(epee::net_utils::http::http_simple_client &http_client, boost::recursive_mutex &mutex)
NodeRPCProxy::NodeRPCProxy(epee::net_utils::http::http_simple_client &http_client, rpc_payment_state_t &rpc_payment_state, boost::recursive_mutex &mutex)
: m_http_client(http_client)
, m_rpc_payment_state(rpc_payment_state)
, m_daemon_rpc_mutex(mutex)
, m_offline(false)
{
@ -58,9 +73,13 @@ void NodeRPCProxy::invalidate()
m_target_height = 0;
m_block_weight_limit = 0;
m_get_info_time = 0;
m_rpc_payment_info_time = 0;
m_rpc_payment_seed_height = 0;
m_rpc_payment_seed_hash = crypto::null_hash;
m_rpc_payment_next_seed_hash = crypto::null_hash;
}
boost::optional<std::string> NodeRPCProxy::get_rpc_version(uint32_t &rpc_version) const
boost::optional<std::string> NodeRPCProxy::get_rpc_version(uint32_t &rpc_version)
{
if (m_offline)
return boost::optional<std::string>("offline");
@ -68,12 +87,11 @@ boost::optional<std::string> NodeRPCProxy::get_rpc_version(uint32_t &rpc_version
{
cryptonote::COMMAND_RPC_GET_VERSION::request req_t = AUTO_VAL_INIT(req_t);
cryptonote::COMMAND_RPC_GET_VERSION::response resp_t = AUTO_VAL_INIT(resp_t);
m_daemon_rpc_mutex.lock();
bool r = net_utils::invoke_http_json_rpc("/json_rpc", "get_version", req_t, resp_t, m_http_client, rpc_timeout);
m_daemon_rpc_mutex.unlock();
CHECK_AND_ASSERT_MES(r, std::string("Failed to connect to daemon"), "Failed to connect to daemon");
CHECK_AND_ASSERT_MES(resp_t.status != CORE_RPC_STATUS_BUSY, resp_t.status, "Failed to connect to daemon");
CHECK_AND_ASSERT_MES(resp_t.status == CORE_RPC_STATUS_OK, resp_t.status, "Failed to get daemon RPC version");
{
const boost::lock_guard<boost::recursive_mutex> lock{m_daemon_rpc_mutex};
bool r = net_utils::invoke_http_json_rpc("/json_rpc", "get_version", req_t, resp_t, m_http_client, rpc_timeout);
RETURN_ON_RPC_RESPONSE_ERROR(r, epee::json_rpc::error{}, resp_t, "get_version");
}
m_rpc_version = resp_t.version;
}
rpc_version = m_rpc_version;
@ -85,7 +103,7 @@ void NodeRPCProxy::set_height(uint64_t h)
m_height = h;
}
boost::optional<std::string> NodeRPCProxy::get_info() const
boost::optional<std::string> NodeRPCProxy::get_info()
{
if (m_offline)
return boost::optional<std::string>("offline");
@ -95,13 +113,15 @@ boost::optional<std::string> NodeRPCProxy::get_info() const
cryptonote::COMMAND_RPC_GET_INFO::request req_t = AUTO_VAL_INIT(req_t);
cryptonote::COMMAND_RPC_GET_INFO::response resp_t = AUTO_VAL_INIT(resp_t);
m_daemon_rpc_mutex.lock();
bool r = net_utils::invoke_http_json_rpc("/json_rpc", "get_info", req_t, resp_t, m_http_client, rpc_timeout);
m_daemon_rpc_mutex.unlock();
{
const boost::lock_guard<boost::recursive_mutex> lock{m_daemon_rpc_mutex};
uint64_t pre_call_credits = m_rpc_payment_state.credits;
req_t.client = cryptonote::make_rpc_payment_signature(m_client_id_secret_key);
bool r = net_utils::invoke_http_json_rpc("/json_rpc", "get_info", req_t, resp_t, m_http_client, rpc_timeout);
RETURN_ON_RPC_RESPONSE_ERROR(r, epee::json_rpc::error{}, resp_t, "get_info");
check_rpc_cost(m_rpc_payment_state, "get_info", resp_t.credits, pre_call_credits, COST_PER_GET_INFO);
}
CHECK_AND_ASSERT_MES(r, std::string("Failed to connect to daemon"), "Failed to connect to daemon");
CHECK_AND_ASSERT_MES(resp_t.status != CORE_RPC_STATUS_BUSY, resp_t.status, "Failed to connect to daemon");
CHECK_AND_ASSERT_MES(resp_t.status == CORE_RPC_STATUS_OK, resp_t.status, "Failed to get target blockchain height");
m_height = resp_t.height;
m_target_height = resp_t.target_height;
m_block_weight_limit = resp_t.block_weight_limit ? resp_t.block_weight_limit : resp_t.block_size_limit;
@ -110,7 +130,7 @@ boost::optional<std::string> NodeRPCProxy::get_info() const
return boost::optional<std::string>();
}
boost::optional<std::string> NodeRPCProxy::get_height(uint64_t &height) const
boost::optional<std::string> NodeRPCProxy::get_height(uint64_t &height)
{
auto res = get_info();
if (res)
@ -119,7 +139,7 @@ boost::optional<std::string> NodeRPCProxy::get_height(uint64_t &height) const
return boost::optional<std::string>();
}
boost::optional<std::string> NodeRPCProxy::get_target_height(uint64_t &height) const
boost::optional<std::string> NodeRPCProxy::get_target_height(uint64_t &height)
{
auto res = get_info();
if (res)
@ -128,7 +148,7 @@ boost::optional<std::string> NodeRPCProxy::get_target_height(uint64_t &height) c
return boost::optional<std::string>();
}
boost::optional<std::string> NodeRPCProxy::get_block_weight_limit(uint64_t &block_weight_limit) const
boost::optional<std::string> NodeRPCProxy::get_block_weight_limit(uint64_t &block_weight_limit)
{
auto res = get_info();
if (res)
@ -137,7 +157,7 @@ boost::optional<std::string> NodeRPCProxy::get_block_weight_limit(uint64_t &bloc
return boost::optional<std::string>();
}
boost::optional<std::string> NodeRPCProxy::get_earliest_height(uint8_t version, uint64_t &earliest_height) const
boost::optional<std::string> NodeRPCProxy::get_earliest_height(uint8_t version, uint64_t &earliest_height)
{
if (m_offline)
return boost::optional<std::string>("offline");
@ -145,14 +165,17 @@ boost::optional<std::string> NodeRPCProxy::get_earliest_height(uint8_t version,
{
cryptonote::COMMAND_RPC_HARD_FORK_INFO::request req_t = AUTO_VAL_INIT(req_t);
cryptonote::COMMAND_RPC_HARD_FORK_INFO::response resp_t = AUTO_VAL_INIT(resp_t);
m_daemon_rpc_mutex.lock();
req_t.version = version;
bool r = net_utils::invoke_http_json_rpc("/json_rpc", "hard_fork_info", req_t, resp_t, m_http_client, rpc_timeout);
m_daemon_rpc_mutex.unlock();
CHECK_AND_ASSERT_MES(r, std::string("Failed to connect to daemon"), "Failed to connect to daemon");
CHECK_AND_ASSERT_MES(resp_t.status != CORE_RPC_STATUS_BUSY, resp_t.status, "Failed to connect to daemon");
CHECK_AND_ASSERT_MES(resp_t.status == CORE_RPC_STATUS_OK, resp_t.status, "Failed to get hard fork status");
{
const boost::lock_guard<boost::recursive_mutex> lock{m_daemon_rpc_mutex};
uint64_t pre_call_credits = m_rpc_payment_state.credits;
req_t.client = cryptonote::make_rpc_payment_signature(m_client_id_secret_key);
bool r = net_utils::invoke_http_json_rpc("/json_rpc", "hard_fork_info", req_t, resp_t, m_http_client, rpc_timeout);
RETURN_ON_RPC_RESPONSE_ERROR(r, epee::json_rpc::error{}, resp_t, "hard_fork_info");
check_rpc_cost(m_rpc_payment_state, "hard_fork_info", resp_t.credits, pre_call_credits, COST_PER_HARD_FORK_INFO);
}
m_earliest_height[version] = resp_t.earliest_height;
}
@ -160,7 +183,7 @@ boost::optional<std::string> NodeRPCProxy::get_earliest_height(uint8_t version,
return boost::optional<std::string>();
}
boost::optional<std::string> NodeRPCProxy::get_dynamic_base_fee_estimate(uint64_t grace_blocks, uint64_t &fee) const
boost::optional<std::string> NodeRPCProxy::get_dynamic_base_fee_estimate(uint64_t grace_blocks, uint64_t &fee)
{
uint64_t height;
@ -174,14 +197,17 @@ boost::optional<std::string> NodeRPCProxy::get_dynamic_base_fee_estimate(uint64_
{
cryptonote::COMMAND_RPC_GET_BASE_FEE_ESTIMATE::request req_t = AUTO_VAL_INIT(req_t);
cryptonote::COMMAND_RPC_GET_BASE_FEE_ESTIMATE::response resp_t = AUTO_VAL_INIT(resp_t);
m_daemon_rpc_mutex.lock();
req_t.grace_blocks = grace_blocks;
bool r = net_utils::invoke_http_json_rpc("/json_rpc", "get_fee_estimate", req_t, resp_t, m_http_client, rpc_timeout);
m_daemon_rpc_mutex.unlock();
CHECK_AND_ASSERT_MES(r, std::string("Failed to connect to daemon"), "Failed to connect to daemon");
CHECK_AND_ASSERT_MES(resp_t.status != CORE_RPC_STATUS_BUSY, resp_t.status, "Failed to connect to daemon");
CHECK_AND_ASSERT_MES(resp_t.status == CORE_RPC_STATUS_OK, resp_t.status, "Failed to get fee estimate");
{
const boost::lock_guard<boost::recursive_mutex> lock{m_daemon_rpc_mutex};
uint64_t pre_call_credits = m_rpc_payment_state.credits;
req_t.client = cryptonote::make_rpc_payment_signature(m_client_id_secret_key);
bool r = net_utils::invoke_http_json_rpc("/json_rpc", "get_fee_estimate", req_t, resp_t, m_http_client, rpc_timeout);
RETURN_ON_RPC_RESPONSE_ERROR(r, epee::json_rpc::error{}, resp_t, "get_fee_estimate");
check_rpc_cost(m_rpc_payment_state, "get_fee_estimate", resp_t.credits, pre_call_credits, COST_PER_FEE_ESTIMATE);
}
m_dynamic_base_fee_estimate = resp_t.fee;
m_dynamic_base_fee_estimate_cached_height = height;
m_dynamic_base_fee_estimate_grace_blocks = grace_blocks;
@ -192,7 +218,7 @@ boost::optional<std::string> NodeRPCProxy::get_dynamic_base_fee_estimate(uint64_
return boost::optional<std::string>();
}
boost::optional<std::string> NodeRPCProxy::get_fee_quantization_mask(uint64_t &fee_quantization_mask) const
boost::optional<std::string> NodeRPCProxy::get_fee_quantization_mask(uint64_t &fee_quantization_mask)
{
uint64_t height;
@ -206,14 +232,17 @@ boost::optional<std::string> NodeRPCProxy::get_fee_quantization_mask(uint64_t &f
{
cryptonote::COMMAND_RPC_GET_BASE_FEE_ESTIMATE::request req_t = AUTO_VAL_INIT(req_t);
cryptonote::COMMAND_RPC_GET_BASE_FEE_ESTIMATE::response resp_t = AUTO_VAL_INIT(resp_t);
m_daemon_rpc_mutex.lock();
req_t.grace_blocks = m_dynamic_base_fee_estimate_grace_blocks;
bool r = net_utils::invoke_http_json_rpc("/json_rpc", "get_fee_estimate", req_t, resp_t, m_http_client, rpc_timeout);
m_daemon_rpc_mutex.unlock();
CHECK_AND_ASSERT_MES(r, std::string("Failed to connect to daemon"), "Failed to connect to daemon");
CHECK_AND_ASSERT_MES(resp_t.status != CORE_RPC_STATUS_BUSY, resp_t.status, "Failed to connect to daemon");
CHECK_AND_ASSERT_MES(resp_t.status == CORE_RPC_STATUS_OK, resp_t.status, "Failed to get fee estimate");
{
const boost::lock_guard<boost::recursive_mutex> lock{m_daemon_rpc_mutex};
uint64_t pre_call_credits = m_rpc_payment_state.credits;
req_t.client = cryptonote::make_rpc_payment_signature(m_client_id_secret_key);
bool r = net_utils::invoke_http_json_rpc("/json_rpc", "get_fee_estimate", req_t, resp_t, m_http_client, rpc_timeout);
RETURN_ON_RPC_RESPONSE_ERROR(r, epee::json_rpc::error{}, resp_t, "get_fee_estimate");
check_rpc_cost(m_rpc_payment_state, "get_fee_estimate", resp_t.credits, pre_call_credits, COST_PER_FEE_ESTIMATE);
}
m_dynamic_base_fee_estimate = resp_t.fee;
m_dynamic_base_fee_estimate_cached_height = height;
m_fee_quantization_mask = resp_t.quantization_mask;
@ -228,4 +257,65 @@ boost::optional<std::string> NodeRPCProxy::get_fee_quantization_mask(uint64_t &f
return boost::optional<std::string>();
}
boost::optional<std::string> NodeRPCProxy::get_rpc_payment_info(bool mining, bool &payment_required, uint64_t &credits, uint64_t &diff, uint64_t &credits_per_hash_found, cryptonote::blobdata &blob, uint64_t &height, uint64_t &seed_height, crypto::hash &seed_hash, crypto::hash &next_seed_hash, uint32_t &cookie)
{
const time_t now = time(NULL);
if (m_rpc_payment_state.stale || now >= m_rpc_payment_info_time + 5*60 || (mining && now >= m_rpc_payment_info_time + 10)) // re-cache every 10 seconds if mining, 5 minutes otherwise
{
cryptonote::COMMAND_RPC_ACCESS_INFO::request req_t = AUTO_VAL_INIT(req_t);
cryptonote::COMMAND_RPC_ACCESS_INFO::response resp_t = AUTO_VAL_INIT(resp_t);
{
const boost::lock_guard<boost::recursive_mutex> lock{m_daemon_rpc_mutex};
req_t.client = cryptonote::make_rpc_payment_signature(m_client_id_secret_key);
bool r = net_utils::invoke_http_json_rpc("/json_rpc", "rpc_access_info", req_t, resp_t, m_http_client, rpc_timeout);
RETURN_ON_RPC_RESPONSE_ERROR(r, epee::json_rpc::error{}, resp_t, "rpc_access_info");
m_rpc_payment_state.stale = false;
}
m_rpc_payment_diff = resp_t.diff;
m_rpc_payment_credits_per_hash_found = resp_t.credits_per_hash_found;
m_rpc_payment_height = resp_t.height;
m_rpc_payment_seed_height = resp_t.seed_height;
m_rpc_payment_cookie = resp_t.cookie;
if (!epee::string_tools::parse_hexstr_to_binbuff(resp_t.hashing_blob, m_rpc_payment_blob) || m_rpc_payment_blob.size() < 43)
{
MERROR("Invalid hashing blob: " << resp_t.hashing_blob);
return std::string("Invalid hashing blob");
}
if (resp_t.seed_hash.empty())
{
m_rpc_payment_seed_hash = crypto::null_hash;
}
else if (!epee::string_tools::hex_to_pod(resp_t.seed_hash, m_rpc_payment_seed_hash))
{
MERROR("Invalid seed_hash: " << resp_t.seed_hash);
return std::string("Invalid seed hash");
}
if (resp_t.next_seed_hash.empty())
{
m_rpc_payment_next_seed_hash = crypto::null_hash;
}
else if (!epee::string_tools::hex_to_pod(resp_t.next_seed_hash, m_rpc_payment_next_seed_hash))
{
MERROR("Invalid next_seed_hash: " << resp_t.next_seed_hash);
return std::string("Invalid next seed hash");
}
m_rpc_payment_info_time = now;
}
payment_required = m_rpc_payment_diff > 0;
credits = m_rpc_payment_state.credits;
diff = m_rpc_payment_diff;
credits_per_hash_found = m_rpc_payment_credits_per_hash_found;
blob = m_rpc_payment_blob;
height = m_rpc_payment_height;
seed_height = m_rpc_payment_seed_height;
seed_hash = m_rpc_payment_seed_hash;
next_seed_hash = m_rpc_payment_next_seed_hash;
cookie = m_rpc_payment_cookie;
return boost::none;
}
}

View file

@ -32,6 +32,8 @@
#include <boost/thread/mutex.hpp>
#include "include_base_utils.h"
#include "net/http_client.h"
#include "rpc/core_rpc_server_commands_defs.h"
#include "wallet_rpc_helpers.h"
namespace tools
{
@ -39,37 +41,62 @@ namespace tools
class NodeRPCProxy
{
public:
NodeRPCProxy(epee::net_utils::http::http_simple_client &http_client, boost::recursive_mutex &mutex);
NodeRPCProxy(epee::net_utils::http::http_simple_client &http_client, rpc_payment_state_t &rpc_payment_state, boost::recursive_mutex &mutex);
void set_client_secret_key(const crypto::secret_key &skey) { m_client_id_secret_key = skey; }
void invalidate();
void set_offline(bool offline) { m_offline = offline; }
boost::optional<std::string> get_rpc_version(uint32_t &version) const;
boost::optional<std::string> get_height(uint64_t &height) const;
boost::optional<std::string> get_rpc_version(uint32_t &version);
boost::optional<std::string> get_height(uint64_t &height);
void set_height(uint64_t h);
boost::optional<std::string> get_target_height(uint64_t &height) const;
boost::optional<std::string> get_block_weight_limit(uint64_t &block_weight_limit) const;
boost::optional<std::string> get_earliest_height(uint8_t version, uint64_t &earliest_height) const;
boost::optional<std::string> get_dynamic_base_fee_estimate(uint64_t grace_blocks, uint64_t &fee) const;
boost::optional<std::string> get_fee_quantization_mask(uint64_t &fee_quantization_mask) const;
boost::optional<std::string> get_target_height(uint64_t &height);
boost::optional<std::string> get_block_weight_limit(uint64_t &block_weight_limit);
boost::optional<std::string> get_earliest_height(uint8_t version, uint64_t &earliest_height);
boost::optional<std::string> get_dynamic_base_fee_estimate(uint64_t grace_blocks, uint64_t &fee);
boost::optional<std::string> get_fee_quantization_mask(uint64_t &fee_quantization_mask);
boost::optional<std::string> get_rpc_payment_info(bool mining, bool &payment_required, uint64_t &credits, uint64_t &diff, uint64_t &credits_per_hash_found, cryptonote::blobdata &blob, uint64_t &height, uint64_t &seed_height, crypto::hash &seed_hash, crypto::hash &next_seed_hash, uint32_t &cookie);
private:
boost::optional<std::string> get_info() const;
template<typename T> void handle_payment_changes(const T &res, std::true_type) {
if (res.status == CORE_RPC_STATUS_OK || res.status == CORE_RPC_STATUS_PAYMENT_REQUIRED)
m_rpc_payment_state.credits = res.credits;
if (res.top_hash != m_rpc_payment_state.top_hash)
{
m_rpc_payment_state.top_hash = res.top_hash;
m_rpc_payment_state.stale = true;
}
}
template<typename T> void handle_payment_changes(const T &res, std::false_type) {}
private:
boost::optional<std::string> get_info();
epee::net_utils::http::http_simple_client &m_http_client;
rpc_payment_state_t &m_rpc_payment_state;
boost::recursive_mutex &m_daemon_rpc_mutex;
crypto::secret_key m_client_id_secret_key;
bool m_offline;
mutable uint64_t m_height;
mutable uint64_t m_earliest_height[256];
mutable uint64_t m_dynamic_base_fee_estimate;
mutable uint64_t m_dynamic_base_fee_estimate_cached_height;
mutable uint64_t m_dynamic_base_fee_estimate_grace_blocks;
mutable uint64_t m_fee_quantization_mask;
mutable uint32_t m_rpc_version;
mutable uint64_t m_target_height;
mutable uint64_t m_block_weight_limit;
mutable time_t m_get_info_time;
uint64_t m_height;
uint64_t m_earliest_height[256];
uint64_t m_dynamic_base_fee_estimate;
uint64_t m_dynamic_base_fee_estimate_cached_height;
uint64_t m_dynamic_base_fee_estimate_grace_blocks;
uint64_t m_fee_quantization_mask;
uint32_t m_rpc_version;
uint64_t m_target_height;
uint64_t m_block_weight_limit;
time_t m_get_info_time;
time_t m_rpc_payment_info_time;
uint64_t m_rpc_payment_diff;
uint64_t m_rpc_payment_credits_per_hash_found;
cryptonote::blobdata m_rpc_payment_blob;
uint64_t m_rpc_payment_height;
uint64_t m_rpc_payment_seed_height;
crypto::hash m_rpc_payment_seed_hash;
crypto::hash m_rpc_payment_next_seed_hash;
uint32_t m_rpc_payment_cookie;
};
}

File diff suppressed because it is too large Load diff

View file

@ -64,10 +64,21 @@
#include "node_rpc_proxy.h"
#include "message_store.h"
#include "wallet_light_rpc.h"
#include "wallet_rpc_helpers.h"
#undef MONERO_DEFAULT_LOG_CATEGORY
#define MONERO_DEFAULT_LOG_CATEGORY "wallet.wallet2"
#define THROW_ON_RPC_RESPONSE_ERROR(r, error, res, method, ...) \
do { \
handle_payment_changes(res, std::integral_constant<bool, HasCredits<decltype(res)>::Has>()); \
throw_on_rpc_response_error(r, error, res.status, method); \
THROW_WALLET_EXCEPTION_IF(res.status != CORE_RPC_STATUS_OK, ## __VA_ARGS__); \
} while(0)
#define THROW_ON_RPC_RESPONSE_ERROR_GENERIC(r, err, res, method) \
THROW_ON_RPC_RESPONSE_ERROR(r, err, res, method, tools::error::wallet_generic_rpc_error, method, res.status)
class Serialization_portability_wallet_Test;
class wallet_accessor_test;
@ -988,6 +999,9 @@ private:
if(ver < 28)
return;
a & m_cold_key_images;
if(ver < 29)
return;
a & m_rpc_client_secret_key;
}
/*!
@ -1061,6 +1075,14 @@ private:
void device_derivation_path(const std::string &device_derivation_path) { m_device_derivation_path = device_derivation_path; }
const ExportFormat & export_format() const { return m_export_format; }
inline void set_export_format(const ExportFormat& export_format) { m_export_format = export_format; }
bool persistent_rpc_client_id() const { return m_persistent_rpc_client_id; }
void persistent_rpc_client_id(bool persistent) { m_persistent_rpc_client_id = persistent; }
void auto_mine_for_rpc_payment_threshold(float threshold) { m_auto_mine_for_rpc_payment_threshold = threshold; }
float auto_mine_for_rpc_payment_threshold() const { return m_auto_mine_for_rpc_payment_threshold; }
crypto::secret_key get_rpc_client_secret_key() const { return m_rpc_client_secret_key; }
void set_rpc_client_secret_key(const crypto::secret_key &key) { m_rpc_client_secret_key = key; m_node_rpc_proxy.set_client_secret_key(key); }
uint64_t credits_target() const { return m_credits_target; }
void credits_target(uint64_t threshold) { m_credits_target = threshold; }
bool get_tx_key_cached(const crypto::hash &txid, crypto::secret_key &tx_key, std::vector<crypto::secret_key> &additional_tx_keys) const;
void set_tx_key(const crypto::hash &txid, const crypto::secret_key &tx_key, const std::vector<crypto::secret_key> &additional_tx_keys);
@ -1108,15 +1130,15 @@ private:
const transfer_details &get_transfer_details(size_t idx) const;
uint8_t get_current_hard_fork();
void get_hard_fork_info(uint8_t version, uint64_t &earliest_height) const;
bool use_fork_rules(uint8_t version, int64_t early_blocks = 0) const;
int get_fee_algorithm() const;
void get_hard_fork_info(uint8_t version, uint64_t &earliest_height);
bool use_fork_rules(uint8_t version, int64_t early_blocks = 0);
int get_fee_algorithm();
std::string get_wallet_file() const;
std::string get_keys_file() const;
std::string get_daemon_address() const;
const boost::optional<epee::net_utils::http::login>& get_daemon_login() const { return m_daemon_login; }
uint64_t get_daemon_blockchain_height(std::string& err) const;
uint64_t get_daemon_blockchain_height(std::string& err);
uint64_t get_daemon_blockchain_target_height(std::string& err);
/*!
* \brief Calculates the approximate blockchain height from current date/time.
@ -1211,21 +1233,37 @@ private:
uint64_t get_blockchain_height_by_date(uint16_t year, uint8_t month, uint8_t day); // 1<=month<=12, 1<=day<=31
bool is_synced() const;
bool is_synced();
std::vector<std::pair<uint64_t, uint64_t>> estimate_backlog(const std::vector<std::pair<double, double>> &fee_levels);
std::vector<std::pair<uint64_t, uint64_t>> estimate_backlog(uint64_t min_tx_weight, uint64_t max_tx_weight, const std::vector<uint64_t> &fees);
uint64_t get_fee_multiplier(uint32_t priority, int fee_algorithm = -1) const;
uint64_t get_base_fee() const;
uint64_t get_fee_quantization_mask() const;
uint64_t get_min_ring_size() const;
uint64_t get_max_ring_size() const;
uint64_t adjust_mixin(uint64_t mixin) const;
uint64_t get_fee_multiplier(uint32_t priority, int fee_algorithm = -1);
uint64_t get_base_fee();
uint64_t get_fee_quantization_mask();
uint64_t get_min_ring_size();
uint64_t get_max_ring_size();
uint64_t adjust_mixin(uint64_t mixin);
uint32_t adjust_priority(uint32_t priority);
bool is_unattended() const { return m_unattended; }
bool get_rpc_payment_info(bool mining, bool &payment_required, uint64_t &credits, uint64_t &diff, uint64_t &credits_per_hash_found, cryptonote::blobdata &hashing_blob, uint64_t &height, uint64_t &seed_height, crypto::hash &seed_hash, crypto::hash &next_seed_hash, uint32_t &cookie);
bool daemon_requires_payment();
bool make_rpc_payment(uint32_t nonce, uint32_t cookie, uint64_t &credits, uint64_t &balance);
bool search_for_rpc_payment(uint64_t credits_target, const std::function<bool(uint64_t, uint64_t)> &startfunc, const std::function<bool(unsigned)> &contfunc, const std::function<bool(uint64_t)> &foundfunc = NULL, const std::function<void(const std::string&)> &errorfunc = NULL);
template<typename T> void handle_payment_changes(const T &res, std::true_type) {
if (res.status == CORE_RPC_STATUS_OK || res.status == CORE_RPC_STATUS_PAYMENT_REQUIRED)
m_rpc_payment_state.credits = res.credits;
if (res.top_hash != m_rpc_payment_state.top_hash)
{
m_rpc_payment_state.top_hash = res.top_hash;
m_rpc_payment_state.stale = true;
}
}
template<typename T> void handle_payment_changes(const T &res, std::false_type) {}
// Light wallet specific functions
// fetch unspent outs from lw node and store in m_transfers
void light_wallet_get_unspent_outs();
@ -1338,6 +1376,9 @@ private:
void enable_dns(bool enable) { m_use_dns = enable; }
void set_offline(bool offline = true);
uint64_t credits() const { return m_rpc_payment_state.credits; }
void credit_report(uint64_t &expected_spent, uint64_t &discrepancy) const { expected_spent = m_rpc_payment_state.expected_spent; discrepancy = m_rpc_payment_state.discrepancy; }
private:
/*!
* \brief Stores wallet information to wallet file.
@ -1363,7 +1404,7 @@ private:
void pull_blocks(uint64_t start_height, uint64_t& blocks_start_height, const std::list<crypto::hash> &short_chain_history, std::vector<cryptonote::block_complete_entry> &blocks, std::vector<cryptonote::COMMAND_RPC_GET_BLOCKS_FAST::block_output_indices> &o_indices);
void pull_hashes(uint64_t start_height, uint64_t& blocks_start_height, const std::list<crypto::hash> &short_chain_history, std::vector<crypto::hash> &hashes);
void fast_refresh(uint64_t stop_height, uint64_t &blocks_start_height, std::list<crypto::hash> &short_chain_history, bool force = false);
void pull_and_parse_next_blocks(uint64_t start_height, uint64_t &blocks_start_height, std::list<crypto::hash> &short_chain_history, const std::vector<cryptonote::block_complete_entry> &prev_blocks, const std::vector<parsed_block> &prev_parsed_blocks, std::vector<cryptonote::block_complete_entry> &blocks, std::vector<parsed_block> &parsed_blocks, bool &error);
void pull_and_parse_next_blocks(uint64_t start_height, uint64_t &blocks_start_height, std::list<crypto::hash> &short_chain_history, const std::vector<cryptonote::block_complete_entry> &prev_blocks, const std::vector<parsed_block> &prev_parsed_blocks, std::vector<cryptonote::block_complete_entry> &blocks, std::vector<parsed_block> &parsed_blocks, bool &error, std::exception_ptr &exception);
void process_parsed_blocks(uint64_t start_height, const std::vector<cryptonote::block_complete_entry> &blocks, const std::vector<parsed_block> &parsed_blocks, uint64_t& blocks_added, std::map<std::pair<uint64_t, uint64_t>, size_t> *output_tracker_cache = NULL);
uint64_t select_transfers(uint64_t needed_money, std::vector<size_t> unused_transfers_indices, std::vector<size_t>& selected_transfers) const;
bool prepare_file_names(const std::string& file_path);
@ -1379,9 +1420,9 @@ private:
void check_acc_out_precomp(const cryptonote::tx_out &o, const crypto::key_derivation &derivation, const std::vector<crypto::key_derivation> &additional_derivations, size_t i, const is_out_data *is_out_data, tx_scan_info_t &tx_scan_info) const;
void check_acc_out_precomp_once(const cryptonote::tx_out &o, const crypto::key_derivation &derivation, const std::vector<crypto::key_derivation> &additional_derivations, size_t i, const is_out_data *is_out_data, tx_scan_info_t &tx_scan_info, bool &already_seen) const;
void parse_block_round(const cryptonote::blobdata &blob, cryptonote::block &bl, crypto::hash &bl_id, bool &error) const;
uint64_t get_upper_transaction_weight_limit() const;
std::vector<uint64_t> get_unspent_amounts_vector(bool strict) const;
uint64_t get_dynamic_base_fee_estimate() const;
uint64_t get_upper_transaction_weight_limit();
std::vector<uint64_t> get_unspent_amounts_vector(bool strict);
uint64_t get_dynamic_base_fee_estimate();
float get_output_relatedness(const transfer_details &td0, const transfer_details &td1) const;
std::vector<size_t> pick_preferred_rct_inputs(uint64_t needed_money, uint32_t subaddr_account, const std::set<uint32_t> &subaddr_indices) const;
void set_spent(size_t idx, uint64_t height);
@ -1435,7 +1476,10 @@ private:
void on_device_progress(const hw::device_progress& event);
std::string get_rpc_status(const std::string &s) const;
void throw_on_rpc_response_error(const boost::optional<std::string> &status, const char *method) const;
void throw_on_rpc_response_error(bool r, const epee::json_rpc::error &error, const std::string &status, const char *method) const;
std::string get_client_signature() const;
void check_rpc_cost(const char *call, uint64_t post_call_credits, uint64_t pre_credits, double expected_cost);
cryptonote::account_base m_account;
boost::optional<epee::net_utils::http::login> m_daemon_login;
@ -1516,6 +1560,8 @@ private:
bool m_track_uses;
uint32_t m_inactivity_lock_timeout;
BackgroundMiningSetupType m_setup_background_mining;
bool m_persistent_rpc_client_id;
float m_auto_mine_for_rpc_payment_threshold;
bool m_is_initialized;
NodeRPCProxy m_node_rpc_proxy;
std::unordered_set<crypto::hash> m_scanned_pool_txs[2];
@ -1526,6 +1572,9 @@ private:
bool m_use_dns;
bool m_offline;
uint32_t m_rpc_version;
crypto::secret_key m_rpc_client_secret_key;
rpc_payment_state_t m_rpc_payment_state;
uint64_t m_credits_target;
// Aux transaction data from device
std::unordered_map<crypto::hash, std::string> m_tx_device;
@ -1569,7 +1618,7 @@ private:
ExportFormat m_export_format;
};
}
BOOST_CLASS_VERSION(tools::wallet2, 28)
BOOST_CLASS_VERSION(tools::wallet2, 29)
BOOST_CLASS_VERSION(tools::wallet2::transfer_details, 12)
BOOST_CLASS_VERSION(tools::wallet2::multisig_info, 1)
BOOST_CLASS_VERSION(tools::wallet2::multisig_info::LR, 0)

View file

@ -76,6 +76,10 @@ namespace wallet_args
{
return {"wallet-file", wallet_args::tr("Use wallet <arg>"), ""};
}
command_line::arg_descriptor<std::string> arg_rpc_client_secret_key()
{
return {"rpc-client-secret-key", wallet_args::tr("Set RPC client secret key for RPC payments"), ""};
}
const char* tr(const char* str)
{

View file

@ -36,6 +36,7 @@ namespace wallet_args
{
command_line::arg_descriptor<std::string> arg_generate_from_json();
command_line::arg_descriptor<std::string> arg_wallet_file();
command_line::arg_descriptor<std::string> arg_rpc_client_secret_key();
const char* tr(const char* str);

View file

@ -90,6 +90,7 @@ namespace tools
// is_key_image_spent_error
// get_histogram_error
// get_output_distribution
// payment_required
// wallet_files_doesnt_correspond
//
// * - class with protected ctor
@ -781,6 +782,20 @@ namespace tools
const std::string m_status;
};
//----------------------------------------------------------------------------------------------------
struct wallet_coded_rpc_error : public wallet_rpc_error
{
explicit wallet_coded_rpc_error(std::string&& loc, const std::string& request, int code, const std::string& status)
: wallet_rpc_error(std::move(loc), std::string("error ") + std::to_string(code) + (" in ") + request + " RPC: " + status, request),
m_code(code), m_status(status)
{
}
int code() const { return m_code; }
const std::string& status() const { return m_status; }
private:
int m_code;
const std::string m_status;
};
//----------------------------------------------------------------------------------------------------
struct daemon_busy : public wallet_rpc_error
{
explicit daemon_busy(std::string&& loc, const std::string& request)
@ -821,6 +836,14 @@ namespace tools
}
};
//----------------------------------------------------------------------------------------------------
struct payment_required: public wallet_rpc_error
{
explicit payment_required(std::string&& loc, const std::string& request)
: wallet_rpc_error(std::move(loc), "payment required", request)
{
}
};
//----------------------------------------------------------------------------------------------------
struct wallet_files_doesnt_correspond : public wallet_logic_error
{
explicit wallet_files_doesnt_correspond(std::string&& loc, const std::string& keys_file, const std::string& wallet_file)

View file

@ -0,0 +1,94 @@
// Copyright (c) 2018-2019, The Monero Project
//
// All rights reserved.
//
// Redistribution and use in source and binary forms, with or without modification, are
// permitted provided that the following conditions are met:
//
// 1. Redistributions of source code must retain the above copyright notice, this list of
// conditions and the following disclaimer.
//
// 2. Redistributions in binary form must reproduce the above copyright notice, this list
// of conditions and the following disclaimer in the documentation and/or other
// materials provided with the distribution.
//
// 3. Neither the name of the copyright holder nor the names of its contributors may be
// used to endorse or promote products derived from this software without specific
// prior written permission.
//
// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY
// EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
// MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL
// THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
// PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
// INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
// STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF
// THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
#pragma once
#include <type_traits>
namespace
{
// credits to yrp (https://stackoverflow.com/questions/87372/check-if-a-class-has-a-member-function-of-a-given-signature
template <typename T>
struct HasCredits
{
template<typename U, uint64_t (U::*)> struct SFINAE {};
template<typename U> static char Test(SFINAE<U, &U::credits>*);
template<typename U> static int Test(...);
static const bool Has = sizeof(Test<T>(0)) == sizeof(char);
};
}
namespace tools
{
struct rpc_payment_state_t
{
uint64_t credits;
uint64_t expected_spent;
uint64_t discrepancy;
std::string top_hash;
bool stale;
rpc_payment_state_t(): credits(0), expected_spent(0), discrepancy(0), stale(true) {}
};
static inline void check_rpc_cost(rpc_payment_state_t &rpc_payment_state, const char *call, uint64_t post_call_credits, uint64_t pre_call_credits, double expected_cost)
{
uint64_t expected_credits = (uint64_t)expected_cost;
if (expected_credits == 0)
expected_credits = 1;
rpc_payment_state.credits = post_call_credits;
rpc_payment_state.expected_spent += expected_credits;
if (pre_call_credits < post_call_credits)
return;
uint64_t cost = pre_call_credits - post_call_credits;
if (cost == expected_credits)
{
MDEBUG("Call " << call << " cost " << cost << " credits");
return;
}
MWARNING("Call " << call << " cost " << cost << " credits, expected " << expected_credits);
if (cost > expected_credits)
{
uint64_t d = cost - expected_credits;
if (rpc_payment_state.discrepancy > std::numeric_limits<uint64_t>::max() - d)
{
MERROR("Integer overflow in credit discrepancy calculation, setting to max");
rpc_payment_state.discrepancy = std::numeric_limits<uint64_t>::max();
}
else
{
rpc_payment_state.discrepancy += d;
}
}
}
}

View file

@ -0,0 +1,196 @@
// Copyright (c) 2018-2019, The Monero Project
//
// All rights reserved.
//
// Redistribution and use in source and binary forms, with or without modification, are
// permitted provided that the following conditions are met:
//
// 1. Redistributions of source code must retain the above copyright notice, this list of
// conditions and the following disclaimer.
//
// 2. Redistributions in binary form must reproduce the above copyright notice, this list
// of conditions and the following disclaimer in the documentation and/or other
// materials provided with the distribution.
//
// 3. Neither the name of the copyright holder nor the names of its contributors may be
// used to endorse or promote products derived from this software without specific
// prior written permission.
//
// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY
// EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
// MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL
// THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
// PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
// INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
// STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF
// THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
#include <boost/optional/optional.hpp>
#include <boost/utility/value_init.hpp>
#include "include_base_utils.h"
#include "cryptonote_config.h"
#include "wallet_rpc_helpers.h"
#include "wallet2.h"
#include "cryptonote_basic/cryptonote_format_utils.h"
#include "rpc/core_rpc_server_commands_defs.h"
#include "rpc/rpc_payment_signature.h"
#include "misc_language.h"
#include "cryptonote_basic/cryptonote_basic_impl.h"
#include "int-util.h"
#include "crypto/crypto.h"
#include "cryptonote_basic/blobdatatype.h"
#include "common/i18n.h"
#include "common/util.h"
#undef MONERO_DEFAULT_LOG_CATEGORY
#define MONERO_DEFAULT_LOG_CATEGORY "wallet.wallet2.rpc_payments"
#define RPC_PAYMENT_POLL_PERIOD 10 /* seconds*/
namespace tools
{
//----------------------------------------------------------------------------------------------------
std::string wallet2::get_client_signature() const
{
return cryptonote::make_rpc_payment_signature(m_rpc_client_secret_key);
}
//----------------------------------------------------------------------------------------------------
bool wallet2::get_rpc_payment_info(bool mining, bool &payment_required, uint64_t &credits, uint64_t &diff, uint64_t &credits_per_hash_found, cryptonote::blobdata &hashing_blob, uint64_t &height, uint64_t &seed_height, crypto::hash &seed_hash, crypto::hash &next_seed_hash, uint32_t &cookie)
{
boost::optional<std::string> result = m_node_rpc_proxy.get_rpc_payment_info(mining, payment_required, credits, diff, credits_per_hash_found, hashing_blob, height, seed_height, seed_hash, next_seed_hash, cookie);
credits = m_rpc_payment_state.credits;
if (result && *result != CORE_RPC_STATUS_OK)
return false;
return true;
}
//----------------------------------------------------------------------------------------------------
bool wallet2::daemon_requires_payment()
{
bool payment_required = false;
uint64_t credits, diff, credits_per_hash_found, height, seed_height;
uint32_t cookie;
cryptonote::blobdata blob;
crypto::hash seed_hash, next_seed_hash;
return get_rpc_payment_info(false, payment_required, credits, diff, credits_per_hash_found, blob, height, seed_height, seed_hash, next_seed_hash, cookie) && payment_required;
}
//----------------------------------------------------------------------------------------------------
bool wallet2::make_rpc_payment(uint32_t nonce, uint32_t cookie, uint64_t &credits, uint64_t &balance)
{
cryptonote::COMMAND_RPC_ACCESS_SUBMIT_NONCE::request req = AUTO_VAL_INIT(req);
cryptonote::COMMAND_RPC_ACCESS_SUBMIT_NONCE::response res = AUTO_VAL_INIT(res);
req.nonce = nonce;
req.cookie = cookie;
m_daemon_rpc_mutex.lock();
uint64_t pre_call_credits = m_rpc_payment_state.credits;
req.client = get_client_signature();
epee::json_rpc::error error;
bool r = epee::net_utils::invoke_http_json_rpc("/json_rpc", "rpc_access_submit_nonce", req, res, error, m_http_client, rpc_timeout);
m_daemon_rpc_mutex.unlock();
THROW_ON_RPC_RESPONSE_ERROR_GENERIC(r, error, res, "rpc_access_submit_nonce");
THROW_WALLET_EXCEPTION_IF(res.credits < pre_call_credits, error::wallet_internal_error, "RPC payment did not increase balance");
if (m_rpc_payment_state.top_hash != res.top_hash)
{
m_rpc_payment_state.top_hash = res.top_hash;
m_rpc_payment_state.stale = true;
}
m_rpc_payment_state.credits = res.credits;
balance = res.credits;
credits = balance - pre_call_credits;
return true;
}
//----------------------------------------------------------------------------------------------------
bool wallet2::search_for_rpc_payment(uint64_t credits_target, const std::function<bool(uint64_t, uint64_t)> &startfunc, const std::function<bool(unsigned)> &contfunc, const std::function<bool(uint64_t)> &foundfunc, const std::function<void(const std::string&)> &errorfunc)
{
bool need_payment = false;
bool payment_required;
uint64_t credits, diff, credits_per_hash_found, height, seed_height;
uint32_t cookie;
unsigned int n_hashes = 0;
cryptonote::blobdata hashing_blob;
crypto::hash seed_hash, next_seed_hash;
try
{
need_payment = get_rpc_payment_info(false, payment_required, credits, diff, credits_per_hash_found, hashing_blob, height, seed_height, seed_hash, next_seed_hash, cookie) && payment_required && credits < credits_target;
if (!need_payment)
return true;
if (!startfunc(diff, credits_per_hash_found))
return true;
}
catch (const std::exception &e) { return false; }
static std::atomic<uint32_t> nonce(0);
while (contfunc(n_hashes))
{
try
{
need_payment = get_rpc_payment_info(true, payment_required, credits, diff, credits_per_hash_found, hashing_blob, height, seed_height, seed_hash, next_seed_hash, cookie) && payment_required && credits < credits_target;
if (!need_payment)
return true;
}
catch (const std::exception &e) { return false; }
if (hashing_blob.empty())
{
MERROR("Bad hashing blob from daemon");
if (errorfunc)
errorfunc("Bad hashing blob from daemon, trying again");
epee::misc_utils::sleep_no_w(1000);
continue;
}
crypto::hash hash;
const uint32_t local_nonce = nonce++; // wrapping's OK
*(uint32_t*)(hashing_blob.data() + 39) = SWAP32LE(local_nonce);
const uint8_t major_version = hashing_blob[0];
if (major_version >= RX_BLOCK_VERSION)
{
const int miners = 1;
crypto::rx_slow_hash(height, seed_height, seed_hash.data, hashing_blob.data(), hashing_blob.size(), hash.data, miners, 0);
}
else
{
int cn_variant = hashing_blob[0] >= 7 ? hashing_blob[0] - 6 : 0;
crypto::cn_slow_hash(hashing_blob.data(), hashing_blob.size(), hash, cn_variant, height);
}
++n_hashes;
if (cryptonote::check_hash(hash, diff))
{
uint64_t credits, balance;
try
{
make_rpc_payment(local_nonce, cookie, credits, balance);
if (credits != credits_per_hash_found)
{
MERROR("Found nonce, but daemon did not credit us with the expected amount");
if (errorfunc)
errorfunc("Found nonce, but daemon did not credit us with the expected amount");
return false;
}
MDEBUG("Found nonce " << local_nonce << " at diff " << diff << ", gets us " << credits_per_hash_found << ", now " << balance << " credits");
if (!foundfunc(credits))
break;
}
catch (const tools::error::wallet_coded_rpc_error &e)
{
MWARNING("Found a local_nonce at diff " << diff << ", but failed to send it to the daemon");
if (errorfunc)
errorfunc("Found nonce, but daemon errored out with error " + std::to_string(e.code()) + ": " + e.status() + ", continuing");
}
catch (const std::exception &e)
{
MWARNING("Found a local_nonce at diff " << diff << ", but failed to send it to the daemon");
if (errorfunc)
errorfunc("Found nonce, but daemon errored out with: '" + std::string(e.what()) + "', continuing");
}
}
}
return true;
}
//----------------------------------------------------------------------------------------------------
void wallet2::check_rpc_cost(const char *call, uint64_t post_call_credits, uint64_t pre_call_credits, double expected_cost)
{
return tools::check_rpc_cost(m_rpc_payment_state, call, post_call_credits, pre_call_credits, expected_cost);
}
//----------------------------------------------------------------------------------------------------
}

View file

@ -4323,6 +4323,7 @@ public:
const auto arg_wallet_file = wallet_args::arg_wallet_file();
const auto arg_from_json = wallet_args::arg_generate_from_json();
const auto arg_rpc_client_secret_key = wallet_args::arg_rpc_client_secret_key();
const auto wallet_file = command_line::get_arg(vm, arg_wallet_file);
const auto from_json = command_line::get_arg(vm, arg_from_json);
@ -4371,6 +4372,17 @@ public:
return false;
}
if (!command_line::is_arg_defaulted(vm, arg_rpc_client_secret_key))
{
crypto::secret_key client_secret_key;
if (!epee::string_tools::hex_to_pod(command_line::get_arg(vm, arg_rpc_client_secret_key), client_secret_key))
{
MERROR(arg_rpc_client_secret_key.name << ": RPC client secret key should be 32 byte in hex format");
return false;
}
wal->set_rpc_client_secret_key(client_secret_key);
}
bool quit = false;
tools::signal_handler::install([&wal, &quit](int) {
assert(wal);
@ -4469,6 +4481,7 @@ int main(int argc, char** argv) {
const auto arg_wallet_file = wallet_args::arg_wallet_file();
const auto arg_from_json = wallet_args::arg_generate_from_json();
const auto arg_rpc_client_secret_key = wallet_args::arg_rpc_client_secret_key();
po::options_description hidden_options("Hidden");
@ -4482,6 +4495,7 @@ int main(int argc, char** argv) {
command_line::add_arg(desc_params, arg_from_json);
command_line::add_arg(desc_params, arg_wallet_dir);
command_line::add_arg(desc_params, arg_prompt_for_password);
command_line::add_arg(desc_params, arg_rpc_client_secret_key);
daemonizer::init_options(hidden_options, desc_params);
desc_params.add(hidden_options);

View file

@ -50,6 +50,20 @@ target_link_libraries(functional_tests
${CMAKE_THREAD_LIBS_INIT}
${EXTRA_LIBRARIES})
set(make_test_signature_sources
make_test_signature.cc)
add_executable(make_test_signature
${make_test_signature_sources})
target_link_libraries(make_test_signature
PRIVATE
rpc_base
cncrypto
epee
${CMAKE_THREAD_LIBS_INIT}
${EXTRA_LIBRARIES})
execute_process(COMMAND ${PYTHON_EXECUTABLE} "-c" "import requests; print('OK')" OUTPUT_VARIABLE REQUESTS_OUTPUT OUTPUT_STRIP_TRAILING_WHITESPACE)
if (REQUESTS_OUTPUT STREQUAL "OK")
add_test(

View file

@ -101,7 +101,7 @@ class BlockchainTest():
for n in range(blocks):
res_getblock.append(daemon.getblock(height = height + n))
block_header = res_getblock[n].block_header
assert abs(block_header.timestamp - time.time()) < 10 # within 10 seconds
assert abs(block_header.timestamp - time.time()) < 60 # within 60 seconds
assert block_header.height == height + n
assert block_header.orphan_status == False
assert block_header.depth == blocks - n - 1

View file

@ -37,6 +37,7 @@ Test the following RPCs:
"""
from __future__ import print_function
import os
from framework.daemon import Daemon
class DaemonGetInfoTest():
@ -63,7 +64,7 @@ class DaemonGetInfoTest():
# difficulty should be set to 1 for this test
assert 'difficulty' in res.keys()
assert res.difficulty == 1;
assert res.difficulty == int(os.environ['DIFFICULTY'])
# nettype should not be TESTNET
assert 'testnet' in res.keys()

View file

@ -10,7 +10,7 @@ import string
import os
USAGE = 'usage: functional_tests_rpc.py <python> <srcdir> <builddir> [<tests-to-run> | all]'
DEFAULT_TESTS = ['address_book', 'bans', 'blockchain', 'cold_signing', 'daemon_info', 'get_output_distribution', 'integrated_address', 'mining', 'multisig', 'proofs', 'sign_message', 'transfer', 'txpool', 'uri', 'validate_address', 'wallet']
DEFAULT_TESTS = ['address_book', 'bans', 'blockchain', 'cold_signing', 'daemon_info', 'get_output_distribution', 'integrated_address', 'mining', 'multisig', 'proofs', 'rpc_payment', 'sign_message', 'transfer', 'txpool', 'uri', 'validate_address', 'wallet']
try:
python = sys.argv[1]
srcdir = sys.argv[2]
@ -34,12 +34,19 @@ try:
except:
tests = DEFAULT_TESTS
N_MONERODS = 1
N_MONERODS = 2
N_WALLETS = 4
WALLET_DIRECTORY = builddir + "/functional-tests-directory"
DIFFICULTY = 10
monerod_base = [builddir + "/bin/monerod", "--regtest", "--fixed-difficulty", "1", "--offline", "--no-igd", "--p2p-bind-port", "monerod_p2p_port", "--rpc-bind-port", "monerod_rpc_port", "--zmq-rpc-bind-port", "monerod_zmq_port", "--non-interactive", "--disable-dns-checkpoints", "--check-updates", "disabled", "--rpc-ssl", "disabled", "--log-level", "1"]
monerod_base = [builddir + "/bin/monerod", "--regtest", "--fixed-difficulty", str(DIFFICULTY), "--offline", "--no-igd", "--p2p-bind-port", "monerod_p2p_port", "--rpc-bind-port", "monerod_rpc_port", "--zmq-rpc-bind-port", "monerod_zmq_port", "--non-interactive", "--disable-dns-checkpoints", "--check-updates", "disabled", "--rpc-ssl", "disabled", "--log-level", "1"]
monerod_extra = [
[],
["--rpc-payment-address", "44SKxxLQw929wRF6BA9paQ1EWFshNnKhXM3qz6Mo3JGDE2YG3xyzVutMStEicxbQGRfrYvAAYxH6Fe8rnD56EaNwUiqhcwR", "--rpc-payment-difficulty", str(DIFFICULTY), "--rpc-payment-credits", "5000", "--data-dir", builddir + "/functional-tests-directory/monerod1"],
]
wallet_base = [builddir + "/bin/monero-wallet-rpc", "--wallet-dir", WALLET_DIRECTORY, "--rpc-bind-port", "wallet_port", "--disable-rpc-login", "--rpc-ssl", "disabled", "--daemon-ssl", "disabled", "--daemon-port", "18180", "--log-level", "1"]
wallet_extra = [
]
command_lines = []
processes = []
@ -48,11 +55,15 @@ ports = []
for i in range(N_MONERODS):
command_lines.append([str(18180+i) if x == "monerod_rpc_port" else str(18280+i) if x == "monerod_p2p_port" else str(18380+i) if x == "monerod_zmq_port" else x for x in monerod_base])
if i < len(monerod_extra):
command_lines[-1] += monerod_extra[i]
outputs.append(open(builddir + '/tests/functional_tests/monerod' + str(i) + '.log', 'a+'))
ports.append(18180+i)
for i in range(N_WALLETS):
command_lines.append([str(18090+i) if x == "wallet_port" else x for x in wallet_base])
if i < len(wallet_extra):
command_lines[-1] += wallet_extra[i]
outputs.append(open(builddir + '/tests/functional_tests/wallet' + str(i) + '.log', 'a+'))
ports.append(18090+i)
@ -65,6 +76,8 @@ try:
os.environ['PYTHONPATH'] = PYTHONPATH
os.environ['WALLET_DIRECTORY'] = WALLET_DIRECTORY
os.environ['PYTHONIOENCODING'] = 'utf-8'
os.environ['DIFFICULTY'] = str(DIFFICULTY)
os.environ['MAKE_TEST_SIGNATURE'] = builddir + '/tests/functional_tests/make_test_signature'
for i in range(len(command_lines)):
#print('Running: ' + str(command_lines[i]))
processes.append(subprocess.Popen(command_lines[i], stdout = outputs[i]))

View file

@ -0,0 +1,60 @@
// Copyright (c) 2019, The Monero Project
//
// All rights reserved.
//
// Redistribution and use in source and binary forms, with or without modification, are
// permitted provided that the following conditions are met:
//
// 1. Redistributions of source code must retain the above copyright notice, this list of
// conditions and the following disclaimer.
//
// 2. Redistributions in binary form must reproduce the above copyright notice, this list
// of conditions and the following disclaimer in the documentation and/or other
// materials provided with the distribution.
//
// 3. Neither the name of the copyright holder nor the names of its contributors may be
// used to endorse or promote products derived from this software without specific
// prior written permission.
//
// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY
// EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
// MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL
// THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
// PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
// INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
// STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF
// THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
#include <stdio.h>
#include "string_tools.h"
#include "rpc/rpc_payment_signature.h"
int main(int argc, const char **argv)
{
if (argc > 2)
{
fprintf(stderr, "usage: %s <secret_key>\n", argv[0]);
return 1;
}
crypto::secret_key skey;
if (argc == 1)
{
crypto::public_key pkey;
crypto::random32_unbiased((unsigned char*)skey.data);
crypto::secret_key_to_public_key(skey, pkey);
printf("%s %s\n", epee::string_tools::pod_to_hex(skey).c_str(), epee::string_tools::pod_to_hex(pkey).c_str());
return 0;
}
if (!epee::string_tools::hex_to_pod(argv[1], skey))
{
fprintf(stderr, "invalid secret key\n");
return 1;
}
std::string signature = cryptonote::make_rpc_payment_signature(skey);
printf("%s\n", signature.c_str());
return 0;
}

View file

@ -92,7 +92,7 @@ class MiningTest():
assert res_status.block_reward >= 600000000000
# wait till we mined a few of them
timeout = 5
timeout = 60 # randomx is slow to init
timeout_height = prev_height
while True:
time.sleep(1)

View file

@ -0,0 +1,412 @@
#!/usr/bin/env python3
# Copyright (c) 2019 The Monero Project
#
# All rights reserved.
#
# Redistribution and use in source and binary forms, with or without modification, are
# permitted provided that the following conditions are met:
#
# 1. Redistributions of source code must retain the above copyright notice, this list of
# conditions and the following disclaimer.
#
# 2. Redistributions in binary form must reproduce the above copyright notice, this list
# of conditions and the following disclaimer in the documentation and/or other
# materials provided with the distribution.
#
# 3. Neither the name of the copyright holder nor the names of its contributors may be
# used to endorse or promote products derived from this software without specific
# prior written permission.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY
# EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
# MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL
# THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
# PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
# STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF
# THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
from __future__ import print_function
import subprocess
import os
"""Test daemon RPC payment calls
"""
from framework.daemon import Daemon
from framework.wallet import Wallet
class RPCPaymentTest():
def run_test(self):
self.make_test_signature = os.environ['MAKE_TEST_SIGNATURE']
assert len(self.make_test_signature) > 0
self.secret_key, self.public_key = self.get_keys()
self.reset()
self.test_access_tracking()
self.test_access_mining()
self.test_access_payment()
self.test_access_account()
self.test_free_access()
def get_keys(self):
output = subprocess.check_output([self.make_test_signature]).rstrip()
fields = output.split()
assert len(fields) == 2
return fields
def get_signature(self):
return subprocess.check_output([self.make_test_signature, self.secret_key]).rstrip()
def reset(self):
print('Resetting blockchain')
daemon = Daemon(idx=1)
res = daemon.get_height()
daemon.pop_blocks(res.height - 1)
daemon.flush_txpool()
def test_access_tracking(self):
print('Testing access tracking')
daemon = Daemon(idx=1)
res = daemon.rpc_access_tracking(True)
res = daemon.rpc_access_tracking()
data = sorted(res.data, key = lambda k: k['rpc'])
assert len(data) == 1
entry = data[0]
assert entry.rpc == 'rpc_access_tracking'
assert entry.count == 1
assert entry.time >= 0
assert entry.credits == 0
daemon.get_connections()
res = daemon.rpc_access_tracking()
data = sorted(res.data, key = lambda k: k['rpc'])
assert len(data) == 2
entry = data[0]
assert entry.rpc == 'get_connections'
assert entry.count == 1
assert entry.time >= 0
assert entry.credits == 0
daemon.get_connections()
res = daemon.rpc_access_tracking()
data = sorted(res.data, key = lambda k: k['rpc'])
assert len(data) == 2
entry = data[0]
assert entry.rpc == 'get_connections'
assert entry.count == 2
assert entry.time >= 0
assert entry.credits == 0
daemon.get_alternate_chains()
res = daemon.rpc_access_tracking()
data = sorted(res.data, key = lambda k: k['rpc'])
assert len(data) == 3
entry = data[0]
assert entry.rpc == 'get_alternate_chains'
assert entry.count == 1
assert entry.time >= 0
assert entry.credits == 0
entry = res.data[1]
assert entry.rpc == 'get_connections'
assert entry.count == 2
assert entry.time >= 0
assert entry.credits == 0
res = daemon.rpc_access_tracking(True)
res = daemon.rpc_access_tracking()
data = sorted(res.data, key = lambda k: k['rpc'])
assert len(data) == 1
entry = data[0]
assert entry.rpc == 'rpc_access_tracking'
assert entry.count == 1
def test_access_mining(self):
print('Testing access mining')
daemon = Daemon(idx=1)
wallet = Wallet(idx=3)
res = daemon.rpc_access_info(client = self.get_signature())
assert len(res.hashing_blob) > 39
assert res.height == 1
assert res.top_hash == '418015bb9ae982a1975da7d79277c2705727a56894ba0fb246adaabb1f4632e3'
assert res.credits_per_hash_found == 5000
assert res.diff == 10
assert res.credits == 0
cookie = res.cookie
# Try random nonces till we find one that's valid and one that's invalid
nonce = 0
found_valid = 0
found_invalid = 0
last_credits = 0
while found_valid == 0 or found_invalid == 0:
nonce += 1
try:
res = daemon.rpc_access_submit_nonce(nonce = nonce, cookie = cookie, client = self.get_signature())
found_valid += 1
assert res.credits == last_credits + 5000
except Exception as e:
found_invalid += 1
res = daemon.rpc_access_info(client = self.get_signature())
assert res.credits < last_credits or res.credits == 0
assert nonce < 1000 # can't find both valid and invalid -> the RPC probably fails
last_credits = res.credits
# we should now have 1 valid nonce, and a number of bad ones
res = daemon.rpc_access_info(client = self.get_signature())
assert len(res.hashing_blob) > 39
assert res.height > 1
assert res.top_hash != '418015bb9ae982a1975da7d79277c2705727a56894ba0fb246adaabb1f4632e3' # here, any share matches network diff
assert res.credits_per_hash_found == 5000
assert res.diff == 10
cookie = res.cookie
res = daemon.rpc_access_data()
assert len(res.entries) > 0
e = [x for x in res.entries if x['client'] == self.public_key]
assert len(e) == 1
e = e[0]
assert e.nonces_stale == 0
assert e.nonces_bad == found_invalid
assert e.nonces_good == found_valid
assert e.nonces_dupe == 0
# Try random nonces till we find one that's valid so we get a load of credits
while last_credits == 0:
nonce += 1
try:
res = daemon.rpc_access_submit_nonce(nonce = nonce, cookie = cookie, client = self.get_signature())
found_valid += 1
last_credits = res.credits
break
except:
found_invalid += 1
assert nonce < 1000 # can't find a valid none -> the RPC probably fails
# we should now have at least 5000
res = daemon.rpc_access_info(client = self.get_signature())
assert res.credits == last_credits
assert res.credits >= 5000 # last one was a valid nonce
res = daemon.rpc_access_data()
assert len(res.entries) > 0
e = [x for x in res.entries if x['client'] == self.public_key]
assert len(e) == 1
e = e[0]
assert e.nonces_stale == 0
assert e.nonces_bad == found_invalid
assert e.nonces_good == found_valid
assert e.nonces_dupe == 0
assert e.balance == 5000
assert e.credits_total >= 5000
# find a valid one, then check dupes aren't allowed
res = daemon.rpc_access_info(client = self.get_signature())
cookie = res.cookie
old_cookie = cookie # we keep that so can submit a stale later
while True:
nonce += 1
try:
res = daemon.rpc_access_submit_nonce(nonce = nonce, cookie = cookie, client = self.get_signature())
found_valid += 1
break
except:
found_invalid += 1
assert nonce < 1000 # can't find both valid and invalid -> the RPC probably fails
res = daemon.rpc_access_data()
assert len(res.entries) > 0
e = [x for x in res.entries if x['client'] == self.public_key]
assert len(e) == 1
e = e[0]
assert e.nonces_stale == 0
assert e.nonces_bad == found_invalid
assert e.nonces_good == found_valid
assert e.nonces_dupe == 0
ok = False
try:
res = daemon.rpc_access_submit_nonce(nonce = nonce, cookie = cookie, client = self.get_signature())
except:
ok = True
assert ok
res = daemon.rpc_access_data()
assert len(res.entries) > 0
e = [x for x in res.entries if x['client'] == self.public_key]
assert len(e) == 1
e = e[0]
assert e.nonces_stale == 0
assert e.nonces_bad == found_invalid
assert e.nonces_good == found_valid
assert e.nonces_dupe == 1
# find stales without updating cookie, one within 5 seconds (accepted), one later (rejected)
res = daemon.rpc_access_info(client = self.get_signature())
found_close_stale = 0
found_late_stale = 0
while found_close_stale == 0 or found_late_stale == 0:
nonce += 1
try:
res = daemon.rpc_access_submit_nonce(nonce = nonce, cookie = cookie, client = self.get_signature())
found_close_stale += 1
found_valid += 1
except Exception as e:
if e[0]['error']['code'] == -18: # stale
found_late_stale += 1
else:
found_invalid += 1
assert nonce < 1000 # can't find both valid and invalid -> the RPC probably fails
res = daemon.rpc_access_data()
assert len(res.entries) > 0
e = [x for x in res.entries if x['client'] == self.public_key]
assert len(e) == 1
e = e[0]
assert e.nonces_stale == found_late_stale # close stales are accepted, don't count here
assert e.nonces_bad == found_invalid
assert e.nonces_good == found_valid
assert e.nonces_dupe == 1
# find very stale with old cookie (rejected)
res = daemon.rpc_access_info(client = self.get_signature())
nonce += 1
ok = False
try:
res = daemon.rpc_access_submit_nonce(nonce = nonce, cookie = old_cookie, client = self.get_signature())
except:
found_late_stale += 1
ok = True
assert ok
res = daemon.rpc_access_data()
assert len(res.entries) > 0
e = [x for x in res.entries if x['client'] == self.public_key]
assert len(e) == 1
e = e[0]
assert e.nonces_stale == found_late_stale
assert e.nonces_bad == found_invalid
assert e.nonces_good == found_valid
assert e.nonces_dupe == 1
def test_access_payment(self):
print('Testing access payment')
daemon = Daemon(idx=1)
wallet = Wallet(idx=3)
# Try random nonces till we find one that's valid so we get a load of credits
res = daemon.rpc_access_info(client = self.get_signature())
credits = res.credits
cookie = res.cookie
nonce = 0
while credits <= 100:
nonce += 1
try:
res = daemon.rpc_access_submit_nonce(nonce = nonce, cookie = cookie, client = self.get_signature())
break
except:
pass
assert nonce < 1000 # can't find both valid and invalid -> the RPC probably fails
res = daemon.rpc_access_info(client = self.get_signature())
credits = res.credits
assert credits > 0
res = daemon.get_info(client = self.get_signature())
assert res.credits == credits - 1
credits = res.credits
res = daemon.generateblocks('42ey1afDFnn4886T7196doS9GPMzexD9gXpsZJDwVjeRVdFCSoHnv7KPbBeGpzJBzHRCAs9UxqeoyFQMYbqSWYTfJJQAWDm', 100)
block_hashes = res.blocks
# ask for 1 block -> 1 credit
res = daemon.getblockheadersrange(0, 0, client = self.get_signature())
assert res.credits == credits - 1
credits = res.credits
# ask for 100 blocks -> >1 credit
res = daemon.getblockheadersrange(1, 100, client = self.get_signature())
assert res.credits < credits - 1
credits = res.credits
# external users
res = daemon.rpc_access_pay(payment = 1, paying_for = 'foo', client = self.get_signature())
assert res.credits == credits - 1
res = daemon.rpc_access_pay(payment = 4, paying_for = 'bar', client = self.get_signature())
assert res.credits == credits - 5
res = daemon.rpc_access_pay(payment = credits, paying_for = 'baz', client = self.get_signature())
assert "PAYMENT REQUIRED" in res.status
res = daemon.rpc_access_pay(payment = 2, paying_for = 'quux', client = self.get_signature())
assert res.credits == credits - 7
res = daemon.rpc_access_pay(payment = 3, paying_for = 'bar', client = self.get_signature())
assert res.credits == credits - 10
# that should be rejected because its cost is massive
ok = False
try: res = daemon.get_output_histogram(amounts = [], client = self.get_signature())
except Exception as e: print('e: ' + str(e)); ok = "PAYMENT REQUIRED" in e.status
assert ok or "PAYMENT REQUIRED" in res.status
def test_access_account(self):
print('Testing access account')
daemon = Daemon(idx=1)
wallet = Wallet(idx=3)
res = daemon.rpc_access_info(client = self.get_signature())
credits = res.credits
res = daemon.rpc_access_account(self.get_signature(), 0)
assert res.credits == credits
res = daemon.rpc_access_account(self.get_signature(), 50)
assert res.credits == credits + 50
res = daemon.rpc_access_account(self.get_signature(), -10)
assert res.credits == credits + 40
res = daemon.rpc_access_account(self.get_signature(), -(credits + 50))
assert res.credits == 0
res = daemon.rpc_access_account(self.get_signature(), 2**63 - 5)
assert res.credits == 2**63 - 5
res = daemon.rpc_access_account(self.get_signature(), 2**63 - 1)
assert res.credits == 2**64 - 6
res = daemon.rpc_access_account(self.get_signature(), 2)
assert res.credits == 2**64 - 4
res = daemon.rpc_access_account(self.get_signature(), 8)
assert res.credits == 2**64 - 1
res = daemon.rpc_access_account(self.get_signature(), -1)
assert res.credits == 2**64 - 2
res = daemon.rpc_access_account(self.get_signature(), -(2**63 - 1))
assert res.credits == 2**64 - 2 -(2**63 - 1)
res = daemon.rpc_access_account(self.get_signature(), -(2**63 - 1))
assert res.credits == 0
def test_free_access(self):
print('Testing free access')
daemon = Daemon(idx=0)
wallet = Wallet(idx=0)
res = daemon.rpc_access_info(client = self.get_signature())
assert res.credits_per_hash_found == 0
assert res.diff == 0
assert res.credits == 0
res = daemon.get_info(client = self.get_signature())
assert res.credits == 0
# any nonce will do here
res = daemon.rpc_access_submit_nonce(nonce = 0, cookie = 0, client = self.get_signature())
assert res.credits == 0
class Guard:
def __enter__(self):
for i in range(4):
Wallet(idx = i).auto_refresh(False)
def __exit__(self, exc_type, exc_value, traceback):
for i in range(4):
Wallet(idx = i).auto_refresh(True)
if __name__ == '__main__':
with Guard() as guard:
RPCPaymentTest().run_test()

View file

@ -37,10 +37,11 @@ class Daemon(object):
self.port = port
self.rpc = JSONRPC('{protocol}://{host}:{port}'.format(protocol=protocol, host=host, port=port if port else 18180+idx))
def getblocktemplate(self, address, prev_block = ""):
def getblocktemplate(self, address, prev_block = "", client = ""):
getblocktemplate = {
'method': 'getblocktemplate',
'params': {
'client': client,
'wallet_address': address,
'reserve_size' : 1,
'prev_block' : prev_block,
@ -51,8 +52,9 @@ class Daemon(object):
return self.rpc.send_json_rpc_request(getblocktemplate)
get_block_template = getblocktemplate
def send_raw_transaction(self, tx_as_hex, do_not_relay = False, do_sanity_checks = True):
def send_raw_transaction(self, tx_as_hex, do_not_relay = False, do_sanity_checks = True, client = ""):
send_raw_transaction = {
'client': client,
'tx_as_hex': tx_as_hex,
'do_not_relay': do_not_relay,
'do_sanity_checks': do_sanity_checks,
@ -70,10 +72,11 @@ class Daemon(object):
return self.rpc.send_json_rpc_request(submitblock)
submit_block = submitblock
def getblock(self, hash = '', height = 0, fill_pow_hash = False):
def getblock(self, hash = '', height = 0, fill_pow_hash = False, client = ""):
getblock = {
'method': 'getblock',
'params': {
'client': client,
'hash': hash,
'height': height,
'fill_pow_hash': fill_pow_hash,
@ -84,10 +87,11 @@ class Daemon(object):
return self.rpc.send_json_rpc_request(getblock)
get_block = getblock
def getlastblockheader(self):
def getlastblockheader(self, client = ""):
getlastblockheader = {
'method': 'getlastblockheader',
'params': {
'client': client,
},
'jsonrpc': '2.0',
'id': '0'
@ -95,10 +99,11 @@ class Daemon(object):
return self.rpc.send_json_rpc_request(getlastblockheader)
get_last_block_header = getlastblockheader
def getblockheaderbyhash(self, hash = "", hashes = []):
def getblockheaderbyhash(self, hash = "", hashes = [], client = ""):
getblockheaderbyhash = {
'method': 'getblockheaderbyhash',
'params': {
'client': client,
'hash': hash,
'hashes': hashes,
},
@ -108,10 +113,11 @@ class Daemon(object):
return self.rpc.send_json_rpc_request(getblockheaderbyhash)
get_block_header_by_hash = getblockheaderbyhash
def getblockheaderbyheight(self, height):
def getblockheaderbyheight(self, height, client = ""):
getblockheaderbyheight = {
'method': 'getblockheaderbyheight',
'params': {
'client': client,
'height': height,
},
'jsonrpc': '2.0',
@ -120,10 +126,11 @@ class Daemon(object):
return self.rpc.send_json_rpc_request(getblockheaderbyheight)
get_block_header_by_height = getblockheaderbyheight
def getblockheadersrange(self, start_height, end_height, fill_pow_hash = False):
def getblockheadersrange(self, start_height, end_height, fill_pow_hash = False, client = ""):
getblockheadersrange = {
'method': 'getblockheadersrange',
'params': {
'client': client,
'start_height': start_height,
'end_height': end_height,
'fill_pow_hash': fill_pow_hash,
@ -134,28 +141,35 @@ class Daemon(object):
return self.rpc.send_json_rpc_request(getblockheadersrange)
get_block_headers_range = getblockheadersrange
def get_connections(self):
def get_connections(self, client = ""):
get_connections = {
'client': client,
'method': 'get_connections',
'jsonrpc': '2.0',
'id': '0'
}
return self.rpc.send_json_rpc_request(get_connections)
def get_info(self):
def get_info(self, client = ""):
get_info = {
'method': 'get_info',
'jsonrpc': '2.0',
'id': '0'
'method': 'get_info',
'params': {
'client': client,
},
'jsonrpc': '2.0',
'id': '0'
}
return self.rpc.send_json_rpc_request(get_info)
getinfo = get_info
def hard_fork_info(self):
def hard_fork_info(self, client = ""):
hard_fork_info = {
'method': 'hard_fork_info',
'jsonrpc': '2.0',
'id': '0'
'method': 'hard_fork_info',
'params': {
'client': client,
},
'jsonrpc': '2.0',
'id': '0'
}
return self.rpc.send_json_rpc_request(hard_fork_info)
@ -174,7 +188,7 @@ class Daemon(object):
}
return self.rpc.send_json_rpc_request(generateblocks)
def get_height(self):
def get_height(self, client = ""):
get_height = {
'method': 'get_height',
'jsonrpc': '2.0',
@ -208,18 +222,21 @@ class Daemon(object):
}
return self.rpc.send_request('/mining_status', mining_status)
def get_transaction_pool(self):
def get_transaction_pool(self, client = ""):
get_transaction_pool = {
'client': client,
}
return self.rpc.send_request('/get_transaction_pool', get_transaction_pool)
def get_transaction_pool_hashes(self):
def get_transaction_pool_hashes(self, client = ""):
get_transaction_pool_hashes = {
'client': client,
}
return self.rpc.send_request('/get_transaction_pool_hashes', get_transaction_pool_hashes)
def get_transaction_pool_stats(self):
def get_transaction_pool_stats(self, client = ""):
get_transaction_pool_stats = {
'client': client,
}
return self.rpc.send_request('/get_transaction_pool_stats', get_transaction_pool_stats)
@ -263,8 +280,9 @@ class Daemon(object):
}
return self.rpc.send_json_rpc_request(set_bans)
def get_transactions(self, txs_hashes = [], decode_as_json = False, prune = False, split = False):
def get_transactions(self, txs_hashes = [], decode_as_json = False, prune = False, split = False, client = ""):
get_transactions = {
'client': client,
'txs_hashes': txs_hashes,
'decode_as_json': decode_as_json,
'prune': prune,
@ -273,17 +291,19 @@ class Daemon(object):
return self.rpc.send_request('/get_transactions', get_transactions)
gettransactions = get_transactions
def get_outs(self, outputs = [], get_txid = False):
def get_outs(self, outputs = [], get_txid = False, client = ""):
get_outs = {
'client': client,
'outputs': outputs,
'get_txid': get_txid,
}
return self.rpc.send_request('/get_outs', get_outs)
def get_coinbase_tx_sum(self, height, count):
def get_coinbase_tx_sum(self, height, count, client = ""):
get_coinbase_tx_sum = {
'method': 'get_coinbase_tx_sum',
'params': {
'client': client,
'height': height,
'count': count,
},
@ -292,10 +312,11 @@ class Daemon(object):
}
return self.rpc.send_json_rpc_request(get_coinbase_tx_sum)
def get_output_distribution(self, amounts = [], from_height = 0, to_height = 0, cumulative = False, binary = False, compress = False):
def get_output_distribution(self, amounts = [], from_height = 0, to_height = 0, cumulative = False, binary = False, compress = False, client = ""):
get_output_distribution = {
'method': 'get_output_distribution',
'params': {
'client': client,
'amounts': amounts,
'from_height': from_height,
'to_height': to_height,
@ -308,10 +329,11 @@ class Daemon(object):
}
return self.rpc.send_json_rpc_request(get_output_distribution)
def get_output_histogram(self, amounts = [], min_count = 0, max_count = 0, unlocked = False, recent_cutoff = 0):
def get_output_histogram(self, amounts = [], min_count = 0, max_count = 0, unlocked = False, recent_cutoff = 0, client = ""):
get_output_histogram = {
'method': 'get_output_histogram',
'params': {
'client': client,
'amounts': amounts,
'min_count': min_count,
'max_count': max_count,
@ -335,15 +357,17 @@ class Daemon(object):
}
return self.rpc.send_request('/set_log_categories', set_log_categories)
def get_alt_blocks_hashes(self):
def get_alt_blocks_hashes(self, client = ""):
get_alt_blocks_hashes = {
'client': client,
}
return self.rpc.send_request('/get_alt_blocks_hashes', get_alt_blocks_hashes)
def get_alternate_chains(self):
def get_alternate_chains(self, client = ""):
get_alternate_chains = {
'method': 'get_alternate_chains',
'params': {
'client': client,
},
'jsonrpc': '2.0',
'id': '0'
@ -361,9 +385,10 @@ class Daemon(object):
}
return self.rpc.send_json_rpc_request(get_fee_estimate)
def is_key_image_spent(self, key_images = []):
def is_key_image_spent(self, key_images = [], client = ""):
is_key_image_spent = {
'key_images': key_images,
'client': client,
}
return self.rpc.send_request('/is_key_image_spent', is_key_image_spent)
@ -413,7 +438,7 @@ class Daemon(object):
def in_peers(self, in_peers):
in_peers = {
'in_peers': in_peers,
'client': client,
}
return self.rpc.send_request('/in_peers', in_peers)
@ -446,31 +471,34 @@ class Daemon(object):
on_get_block_hash = get_block_hash
on_getblockhash = get_block_hash
def relay_tx(self, txids = []):
def relay_tx(self, txids = [], client = ""):
relay_tx = {
'method': 'relay_tx',
'params': {
'txids': txids,
'client': client,
},
'jsonrpc': '2.0',
'id': '0'
}
return self.rpc.send_json_rpc_request(relay_tx)
def sync_info(self):
def sync_info(self, client = ""):
sync_info = {
'method': 'sync_info',
'params': {
'client': client,
},
'jsonrpc': '2.0',
'id': '0'
}
return self.rpc.send_json_rpc_request(sync_info)
def get_txpool_backlog(self):
def get_txpool_backlog(self, client = ""):
get_txpool_backlog = {
'method': 'get_txpool_backlog',
'params': {
'client': client,
},
'jsonrpc': '2.0',
'id': '0'
@ -498,3 +526,73 @@ class Daemon(object):
'id': '0'
}
return self.rpc.send_json_rpc_request(get_block_rate)
def rpc_access_info(self, client):
rpc_access_info = {
'method': 'rpc_access_info',
'params': {
'client': client,
},
'jsonrpc': '2.0',
'id': '0'
}
return self.rpc.send_json_rpc_request(rpc_access_info)
def rpc_access_submit_nonce(self, client, nonce, cookie):
rpc_access_submit_nonce = {
'method': 'rpc_access_submit_nonce',
'params': {
'client': client,
'nonce': nonce,
'cookie': cookie,
},
'jsonrpc': '2.0',
'id': '0'
}
return self.rpc.send_json_rpc_request(rpc_access_submit_nonce)
def rpc_access_pay(self, client, paying_for, payment):
rpc_access_pay = {
'method': 'rpc_access_pay',
'params': {
'client': client,
'paying_for': paying_for,
'payment': payment,
},
'jsonrpc': '2.0',
'id': '0'
}
return self.rpc.send_json_rpc_request(rpc_access_pay)
def rpc_access_tracking(self, clear = False):
rpc_access_tracking = {
'method': 'rpc_access_tracking',
'params': {
'clear': clear,
},
'jsonrpc': '2.0',
'id': '0'
}
return self.rpc.send_json_rpc_request(rpc_access_tracking)
def rpc_access_data(self):
rpc_access_data = {
'method': 'rpc_access_data',
'params': {
},
'jsonrpc': '2.0',
'id': '0'
}
return self.rpc.send_json_rpc_request(rpc_access_data)
def rpc_access_account(self, client, delta_balance = 0):
rpc_access_account = {
'method': 'rpc_access_account',
'params': {
'client': client,
'delta_balance': delta_balance,
},
'jsonrpc': '2.0',
'id': '0'
}
return self.rpc.send_json_rpc_request(rpc_access_account)